Arquitetando seu aplicativo
Android com o Jetpack
+Nelson Glauber
@nglauber

www.nglauber.com.br

youtube.com/nglauber
Porque pensar na
arquitetura?
‣ Frameworks raramente forçam o desenvolvedor a seguir
princípios.
‣ É fácil um desenvolvedor iniciante sem o devido
acompanhamento criar um app ruim.
‣ Tecnologia e frameworks ocultam o propósito do
aplicativo.
‣ “Sua arquitetura deve gritar o propósito do aplicativo”. A
lógica de negócio deve ser claramente separada e
independente de framework.
Arquitetura
‣ Regra: Não há regras. Mas existem princípios que devem
ser respeitados. Lembra do S.O.L.I.D.?
‣ Promove a organização e o desacoplamento do código.
‣ Devem facilitar a manutenção e adição de novas
funcionalidades.
‣ Devem ser testáveis.
‣ Deixam o código mais complexo? Sim! Mas vale à pena. 😎
MVP
View
IView
Presenter Model
Interação 

do usuário
Request
DadosLógica de UI
implements
IPresenter
implements
MVVM
View ViewModel Model
Observes
Interação 

do usuário
Dados
Request
https://fernandocejas.com/2014/09/03/architecting-android-the-clean-way/
androidx.*
UI (app)
Presentation
Domain
Remote Local
Data
UI (app)
Presentation
Domain
Remote Local
Data
‣ Módulo Kotlin
‣ Normalmente classes puras ou
interfaces
‣ Ser ou não ser reativo aqui… 🤔
Data
RX Java
import dominando.android.data.model.Book
import io.reactivex.Completable
import io.reactivex.Flowable
interface BooksRepository {
fun saveBook(book: Book): Completable
fun loadBooks(): Flowable<List<Book>>
fun loadBook(bookId: String): Flowable<Book>
fun remove(book: Book): Completable
}
UI (app)
Presentation
Domain
Remote Local
Data
Local
Room
‣ ORM para Android sobre o SQLite.
‣ Suporta atualização automática da UI
com LiveData ou RXJava
Local
@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
)
@Dao
interface BookDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun save(book: Book): Completable
@Delete
fun delete(vararg book: Book): Completable
@Query("SELECT * FROM Book WHERE title LIKE :title ORDER BY title")
fun bookByTitle(title: String = "%"): Flowable<List<Book>>
@Query("SELECT * FROM Book WHERE id = :id")
fun bookById(id: String): Flowable<Book>
}
@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
}
}
}
class RoomRepository(db: AppDatabase,
private val fileHelper: FileHelper) : BooksRepository {
private val bookDao = db.bookDao()
override fun saveBook(book: Book): Completable {
if (book.id.isBlank()) {
book.id = UUID.randomUUID().toString()
}
return bookDao.save(BookConverter.fromData(book))
}
override fun loadBooks(): Flowable<List<Book>> {
return bookDao.bookByTitle()
.map { books ->
books.map { book ->
BookConverter.toData(book)
}
}
}
...
UI (app)
Presentation
Domain
Remote Local
Data
‣ Módulo Kotlin
‣ Abstração da lógica de negócio
‣ A lógica deve ser implementada
aqui de forma abstrata.
Domain
open class ListBooksUseCase(
private val repository: BooksRepository,
postExecutionThread: PostExecutionThread
) : FlowableUseCase<List<Book>, Unit>(postExecutionThread) {
override fun buildUseCaseFlowable(params: Unit?)
: Flowable<List<Book>> {
return repository.loadBooks()
}
}
open class SaveBookUseCase(
private val repository: BooksRepository,
postExecutionThread: PostExecutionThread
): CompletableUseCase<Book>(postExecutionThread) {
override fun buildUseCaseCompletable(params: Book?): Completable {
return if (params != null && bookIsValid(params)) {
repository.saveBook(params)
} else {
Completable.error(IllegalArgumentException("Book is invalid"))
}
}
private fun bookIsValid(book: Book): Boolean {
return (
book.title.isNotBlank() &&
book.author.isNotBlank() &&
book.pages > 0 &&
book.year > 1900 &&
book.year <= Calendar.getInstance().get(Calendar.YEAR)
)
}
}
UI (app)
Presentation
Domain
Remote Local
Data
Presentation
Data Binding
• Facilita a ligação entre o
Presenter ou View Model e a
View
• Estende os arquivos de layout
com micro-expressões
• Muito útil em telas de input de
dados
<layout ...>
<data>
...
<import type="br.com.nglauber.livrosfirebase.model.MediaType" />
<variable name="book"
type="br.com.nglauber.livrosfirebase.model.Book" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout ...>
<ImageView android:src="@{book.coverUrl}" ... />
<EditText android:text="@={book.title}" ... />
<EditText android:text="@={book.author}" ... />
<EditText android:text="@={book.pages}" ... />
<EditText android:text="@={book.year}" ... />
<Spinner ...>
<CheckBox android:checked="@={book.available}" ... />
<RadioGroup ...>
<RadioButton ...
android:checked="@{book.mediaTypeValue == MediaType.EBOOK}" ... />
<RadioButton ...
android:checked="@{book.mediaTypeValue == MediaType.PAPER}" ... />
</RadioGroup>
<RatingBar
android:rating="@={book.rating}" ... />
...
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
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
}
...
@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
}
ViewModel
Presentation
LiveData
• LiveData armazena dados observáveis
(Observable) e notifica os observadores
(Observer) quando esses dados mudam
para que a UI seja atualizada.
• O LiveData é lifecycle-aware. Ele só
notificará a UI se a Activity/Fragment
estiver no estado STARTED ou RESUMED.
Presentation
class BookDetailsViewModel(private val useCase: ViewBookDetailsUseCase) : ViewModel() {
private val state: MutableLiveData<ViewState<BookBinding>> = MutableLiveData()
fun getState(): LiveData<ViewState<BookBinding>> = state
fun loadBook(id: String) {
if (id != state.value?.data?.id) {
state.postValue(ViewState(ViewState.Status.LOADING))
useCase.execute(id,
{ book ->
val bookBinding = BookConverter.fromData(book)
state.postValue(ViewState(ViewState.Status.SUCCESS, bookBinding))
},
{ e ->
state.postValue(ViewState(ViewState.Status.ERROR, error = e))
}
)
}
}
override fun onCleared() {
super.onCleared()
useCase.dispose()
}
}
class BookDetailsFragment : BaseFragment() {
...
private fun init() {
viewModel.getState().observe(this, Observer { viewState ->
when (viewState.status) {
ViewState.Status.SUCCESS -> binding.book = viewState.data
ViewState.Status.LOADING -> { /* show progress*/ }
ViewState.Status.ERROR -> { /* show error */}
}
})
val book = arguments?.getParcelable<Book>("book")
book?.let {
viewModel.loadBook(book.id)
}
}
}
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
}
class BookFormViewModel(private val useCase: SaveBookUseCase) : ViewModel() {
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)))
useCase.execute(BookConverter.toData(book),
{
state.postValue(LiveEvent(ViewState(ViewState.Status.SUCCESS)))
},
{ e ->
state.postValue(LiveEvent(ViewState(ViewState.Status.ERROR, error = e)))
}
)
}
class BookFormFragment : BaseFragment() {
...
private fun init() {
viewModel.getState().observe(this, Observer { event ->
event?.consumeEvent()?.let { state ->
when (state.status) {
ViewState.Status.LOADING -> { ... }
ViewState.Status.SUCCESS -> { ... }
ViewState.Status.ERROR -> { ... }
}
}
})
}
Lifecycle
‣ Lifecycle é um objeto que define um ciclo de vida
‣ LifecycleOwner é uma interface para objetos com um
ciclo de vida
‣ Activity e Fragment implementam LifecycleOwner
e têm um Lifecycle.
‣ LifecycleObserver é uma interface para observar um
LifecycleOwner.
class BookListViewModel(
private val loadBooksUseCase: ListBooksUseCase,...
) : ViewModel(), LifecycleObserver {
...
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun loadBooks() {
if (state.value == null) {
state.postValue(ViewState(ViewState.Status.LOADING))
loadBooksUseCase.execute(null,
{ books ->
val booksBinding = books.map { book ->
BookConverter.fromData(book)
}
state.postValue(
ViewState(ViewState.Status.SUCCESS, booksBinding)
)
},
{ e ->
state.postValue(
ViewState(ViewState.Status.ERROR, error = e)
)
}
)
}
}
class BookListFragment : BaseFragment() {
private val viewModel: BookListViewModel
private fun init() {
...
lifecycle.addObserver(viewModel)
}
...
UI (app)
Presentation
Domain
Remote Local
Data
UI (app)
Navigation API
• Serve para implementar o conceito de
“Single Activity” com múltiplos
Fragments/Views.
• Centraliza a lógica de navegação da
aplicação.
• Configuração direta com componentes de
UI (ActionBar, Menu, BottomNav, …)
• Simplifica a passagem de parâmetros.
<navigation ...
android:id="@+id/main_graph"
app:startDestination="@id/listBooks">
<fragment
android:id="@+id/listBooks"
android:name="dominando.android.livros.BookListFragment"
tools:layout="@layout/fragment_book_list"> ...
<action
android:id="@+id/action_list_to_details"
app:destination="@id/bookDetails" />
</fragment>
<fragment
android:id="@+id/bookDetails"
android:name="dominando.android.livros.BookDetailsFragment"
tools:layout="@layout/fragment_book_details"> ...
<argument
android:name="book"
app:argType="dominando.android.presentation.binding.Book"
app:nullable="true" />
</fragment> ...
</navigation>
<FrameLayout ...>
<fragment
android:id="@+id/navHost"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:navGraph="@navigation/nav_graph"
app:defaultNavHost="true" />
</FrameLayout>
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()
}
}
// 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
WorkManager
https://android-developers.googleblog.com/2018/10/modern-background-execution-in-android.html
class MyWork(ctx: Context, params: WorkerParameters):
Worker(ctx, params) {
override fun doWork(): Result {
val firstName = inputData.getString(PARAM_FIRST_NAME)
outputData = Data.Builder()
.putString(PARAM_NAME, "$firstName Glauber")
.putInt(PARAM_AGE, 35)
.putLong(PARAM_TIME, System.currentTimeMillis())
.build()
return Result.SUCCESS
}
companion object {
const val PARAM_FIRST_NAME = "first_name"
const val PARAM_NAME = "name"
const val PARAM_AGE = "age"
const val PARAM_TIME = "time"
}
}
val input = Data.Builder()
.putString(MyWork.PARAM_FIRST_NAME, "Nelson")
.build()
// Uma vez
val request = OneTimeWorkRequest.Builder(MyWork::class.java)
.setInputData(input)
.build()
// ou Periódico
val request = PeriodicWorkRequest.Builder(
MyWork::class.java, 5, TimeUnit.MINUTES)
.setInputData(input)
.build()
observeAndEnqueue(request)
private val wm = WorkManager.getInstance()
private fun observeAndEnqueue(request: WorkRequest) {
wm.enqueue(request)
workId = request.id
wm.getWorkInfoByIdLiveData(request.id)
.observe(this, Observer { status ->
txtStatus.text = when (status?.state) {
WorkInfo.State.ENQUEUED -> "Enfileirado"
WorkInfo.State.BLOCKED -> "Bloqueado"
WorkInfo.State.CANCELLED -> "Cancelado"
WorkInfo.State.RUNNING -> "Executando"
WorkInfo.State.SUCCEEDED -> "Sucesso"
WorkInfo.State.FAILED -> "Falhou"
else -> "Indefinido"
}
txtOutput.text = status?.outputData?.run {
getString(MyWork.PARAM_NAME) +
getInt(MyWork.PARAM_AGE, 0) +
getLong(MyWork.PARAM_TIME, 0)
}
})
}
// Cancelando
workId?.let { uuid ->
wm.cancelWorkById(uuid)
}
wm.cancelAllWork()
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.NOT_REQUIRED)
.setRequiresBatteryNotLow(false)
.setRequiresCharging(false)
.setRequiresStorageNotLow(false)
.build()
val request = OneTimeWorkRequest.Builder(MyWork::class.java)
.setConstraints(constraints)
.build()
val request0 = OneTimeWorkRequest.Builder(MyWork::class.java).build()
val request1 = OneTimeWorkRequest.Builder(Worker1::class.java).build()
val request2 = OneTimeWorkRequest.Builder(Worker2::class.java).build()
val request3 = OneTimeWorkRequest.Builder(Worker3::class.java).build()
wm.beginWith(request0)
.then(listOf(request1, request2))
.then(request3)
.enqueue()
Paging
class CharactersDataSourceFactory(
private val compositeDisposable: CompositeDisposable,
private val marvelApi: MarvelApi
) : DataSource.Factory<Int, Character>() {
override fun create(): DataSource<Int, Character> {
return CharactersDataSource(marvelApi, compositeDisposable)
}
}
class CharactersDataSource(
private val marvelApi: MarvelApi,
private val compositeDisposable: CompositeDisposable
) : PageKeyedDataSource<Int, Character>() {
// PositionalDataSource<Character>
// ItemKeyedDataSource<Int,Character>
override fun loadInitial(params: LoadInitialParams<Int>,
callback: LoadInitialCallback<Int, Character>) {
callback?.onResult(response.data.results, null, 1)
}
override fun loadAfter(params: LoadParams<Int>,
callback: LoadCallback<Int, Character>) {
callback?.onResult(response.data.results, params.key + 1)
}
override fun loadBefore(params: LoadParams<Int>,
callback: LoadCallback<Int, Character>) {
callback?.onResult(response.data.results, params.key - 1)
}
}
class CharactersViewModel : ViewModel() {
var characterList: Observable<PagedList<Character>>
private val compositeDisposable = CompositeDisposable()
private val sourceFactory: CharactersDataSourceFactory
init {
sourceFactory = CharactersDataSourceFactory(
compositeDisposable, MarvelApi.getService()
)
val config = PagedList.Config.Builder()
.setPageSize(20)
.setInitialLoadSizeHint(40)
.setPrefetchDistance(10)
.setEnablePlaceholders(false)
.build()
characterList = RxPagedListBuilder(sourceFactory, config)
.setFetchScheduler(Schedulers.io())
.buildObservable()
.cache()
}
...
}
class CharactersAdapter() : PagedListAdapter<Character, CharactersAdapter.VH>(CharacterDiffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_character, parent, false)
return VH(view)
}
override fun onBindViewHolder(holder: VH, position: Int) {
val character = getItem(position)
holder.txtName.text = character?.name
holder.imgThumbnail.load(character?.thumbnail?.path)
}
class VH(itemView: View) : RecyclerView.ViewHolder(itemView) {
val imgThumbnail = itemView.imgThumbnail
val txtName = itemView.txtName
}
companion object {
val CharacterDiffCallback = object : DiffUtil.ItemCallback<Character>() {
override fun areItemsTheSame(oldItem: Character, newItem: Character): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Character, newItem: Character): Boolean {
return oldItem == newItem
}
}
}
}
#
#
🧐 #
#
#
#
🧐 #
🧐 #
• Você não precisa usar todos esses
recursos no mesmo app…
• Na verdade, você não precisa usar
nenhum!
• Mais importante do que saber quando
USAR é saber quando NÃO USAR 😉
• Mas é essencial conhecer tópicos,
saber seus prós e contras e utilizá-los
adequadamente 💡
github.com/googlesamples/android-architecture
Referências
• Android Architecture Components

https://developer.android.com/topic/libraries/architecture/
• LiveData beyond the ViewModel

https://medium.com/androiddevelopers/livedata-beyond-the-
viewmodel-reactive-patterns-using-transformations-and-
mediatorlivedata-fda520ba00b7
• Android Architecture (Five Agency)

https://five.agency/android-architecture-part-1-every-new-
beginning-is-hard/
• Joe Birch (@hitherejoe) Course at Caster.io

https://caster.io/courses/android-clean-architecture
@nglauber
+NelsonGlauber
www.nglauber.com.br

Obrigado!
youtube.com/nglauber

Arquitetando seu aplicativo Android com Jetpack

  • 1.
    Arquitetando seu aplicativo Androidcom o Jetpack +Nelson Glauber @nglauber
 www.nglauber.com.br
 youtube.com/nglauber
  • 2.
    Porque pensar na arquitetura? ‣Frameworks raramente forçam o desenvolvedor a seguir princípios. ‣ É fácil um desenvolvedor iniciante sem o devido acompanhamento criar um app ruim. ‣ Tecnologia e frameworks ocultam o propósito do aplicativo. ‣ “Sua arquitetura deve gritar o propósito do aplicativo”. A lógica de negócio deve ser claramente separada e independente de framework.
  • 3.
    Arquitetura ‣ Regra: Nãohá regras. Mas existem princípios que devem ser respeitados. Lembra do S.O.L.I.D.? ‣ Promove a organização e o desacoplamento do código. ‣ Devem facilitar a manutenção e adição de novas funcionalidades. ‣ Devem ser testáveis. ‣ Deixam o código mais complexo? Sim! Mas vale à pena. 😎
  • 4.
    MVP View IView Presenter Model Interação 
 dousuário Request DadosLógica de UI implements IPresenter implements
  • 6.
  • 9.
  • 10.
  • 14.
  • 15.
  • 16.
    ‣ Módulo Kotlin ‣Normalmente classes puras ou interfaces ‣ Ser ou não ser reativo aqui… 🤔 Data
  • 17.
  • 18.
    import dominando.android.data.model.Book import io.reactivex.Completable importio.reactivex.Flowable interface BooksRepository { fun saveBook(book: Book): Completable fun loadBooks(): Flowable<List<Book>> fun loadBook(bookId: String): Flowable<Book> fun remove(book: Book): Completable }
  • 19.
  • 20.
  • 21.
    Room ‣ ORM paraAndroid sobre o SQLite. ‣ Suporta atualização automática da UI com LiveData ou RXJava Local
  • 23.
    @Entity @TypeConverters(MediaTypeConverter::class) data class Book( @PrimaryKey varid: 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 )
  • 24.
    @Dao interface BookDao { @Insert(onConflict= OnConflictStrategy.REPLACE) fun save(book: Book): Completable @Delete fun delete(vararg book: Book): Completable @Query("SELECT * FROM Book WHERE title LIKE :title ORDER BY title") fun bookByTitle(title: String = "%"): Flowable<List<Book>> @Query("SELECT * FROM Book WHERE id = :id") fun bookById(id: String): Flowable<Book> }
  • 25.
    @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 } } }
  • 26.
    class RoomRepository(db: AppDatabase, privateval fileHelper: FileHelper) : BooksRepository { private val bookDao = db.bookDao() override fun saveBook(book: Book): Completable { if (book.id.isBlank()) { book.id = UUID.randomUUID().toString() } return bookDao.save(BookConverter.fromData(book)) } override fun loadBooks(): Flowable<List<Book>> { return bookDao.bookByTitle() .map { books -> books.map { book -> BookConverter.toData(book) } } } ...
  • 27.
  • 28.
    ‣ Módulo Kotlin ‣Abstração da lógica de negócio ‣ A lógica deve ser implementada aqui de forma abstrata. Domain
  • 29.
    open class ListBooksUseCase( privateval repository: BooksRepository, postExecutionThread: PostExecutionThread ) : FlowableUseCase<List<Book>, Unit>(postExecutionThread) { override fun buildUseCaseFlowable(params: Unit?) : Flowable<List<Book>> { return repository.loadBooks() } }
  • 30.
    open class SaveBookUseCase( privateval repository: BooksRepository, postExecutionThread: PostExecutionThread ): CompletableUseCase<Book>(postExecutionThread) { override fun buildUseCaseCompletable(params: Book?): Completable { return if (params != null && bookIsValid(params)) { repository.saveBook(params) } else { Completable.error(IllegalArgumentException("Book is invalid")) } } private fun bookIsValid(book: Book): Boolean { return ( book.title.isNotBlank() && book.author.isNotBlank() && book.pages > 0 && book.year > 1900 && book.year <= Calendar.getInstance().get(Calendar.YEAR) ) } }
  • 31.
  • 32.
    Presentation Data Binding • Facilitaa ligação entre o Presenter ou View Model e a View • Estende os arquivos de layout com micro-expressões • Muito útil em telas de input de dados
  • 34.
    <layout ...> <data> ... <import type="br.com.nglauber.livrosfirebase.model.MediaType"/> <variable name="book" type="br.com.nglauber.livrosfirebase.model.Book" /> </data> <androidx.constraintlayout.widget.ConstraintLayout ...> <ImageView android:src="@{book.coverUrl}" ... /> <EditText android:text="@={book.title}" ... /> <EditText android:text="@={book.author}" ... /> <EditText android:text="@={book.pages}" ... /> <EditText android:text="@={book.year}" ... /> <Spinner ...> <CheckBox android:checked="@={book.available}" ... /> <RadioGroup ...> <RadioButton ... android:checked="@{book.mediaTypeValue == MediaType.EBOOK}" ... /> <RadioButton ... android:checked="@{book.mediaTypeValue == MediaType.PAPER}" ... /> </RadioGroup> <RatingBar android:rating="@={book.rating}" ... /> ... </androidx.constraintlayout.widget.ConstraintLayout> </layout>
  • 35.
    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 } ...
  • 36.
    @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 }
  • 37.
  • 38.
    LiveData • LiveData armazenadados observáveis (Observable) e notifica os observadores (Observer) quando esses dados mudam para que a UI seja atualizada. • O LiveData é lifecycle-aware. Ele só notificará a UI se a Activity/Fragment estiver no estado STARTED ou RESUMED. Presentation
  • 39.
    class BookDetailsViewModel(private valuseCase: ViewBookDetailsUseCase) : ViewModel() { private val state: MutableLiveData<ViewState<BookBinding>> = MutableLiveData() fun getState(): LiveData<ViewState<BookBinding>> = state fun loadBook(id: String) { if (id != state.value?.data?.id) { state.postValue(ViewState(ViewState.Status.LOADING)) useCase.execute(id, { book -> val bookBinding = BookConverter.fromData(book) state.postValue(ViewState(ViewState.Status.SUCCESS, bookBinding)) }, { e -> state.postValue(ViewState(ViewState.Status.ERROR, error = e)) } ) } } override fun onCleared() { super.onCleared() useCase.dispose() } }
  • 40.
    class BookDetailsFragment :BaseFragment() { ... private fun init() { viewModel.getState().observe(this, Observer { viewState -> when (viewState.status) { ViewState.Status.SUCCESS -> binding.book = viewState.data ViewState.Status.LOADING -> { /* show progress*/ } ViewState.Status.ERROR -> { /* show error */} } }) val book = arguments?.getParcelable<Book>("book") book?.let { viewModel.loadBook(book.id) } } }
  • 41.
    Presentation open class LiveEvent<outT>(private val content: T) { var hasBeenConsumed = false private set fun consumeEvent(): T? { return if (hasBeenConsumed) { null } else { hasBeenConsumed = true content } } fun peekContent(): T = content }
  • 42.
    class BookFormViewModel(private valuseCase: SaveBookUseCase) : ViewModel() { 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))) useCase.execute(BookConverter.toData(book), { state.postValue(LiveEvent(ViewState(ViewState.Status.SUCCESS))) }, { e -> state.postValue(LiveEvent(ViewState(ViewState.Status.ERROR, error = e))) } ) }
  • 43.
    class BookFormFragment :BaseFragment() { ... private fun init() { viewModel.getState().observe(this, Observer { event -> event?.consumeEvent()?.let { state -> when (state.status) { ViewState.Status.LOADING -> { ... } ViewState.Status.SUCCESS -> { ... } ViewState.Status.ERROR -> { ... } } } }) }
  • 44.
    Lifecycle ‣ Lifecycle éum objeto que define um ciclo de vida ‣ LifecycleOwner é uma interface para objetos com um ciclo de vida ‣ Activity e Fragment implementam LifecycleOwner e têm um Lifecycle. ‣ LifecycleObserver é uma interface para observar um LifecycleOwner.
  • 45.
    class BookListViewModel( private valloadBooksUseCase: ListBooksUseCase,... ) : ViewModel(), LifecycleObserver { ... @OnLifecycleEvent(Lifecycle.Event.ON_CREATE) fun loadBooks() { if (state.value == null) { state.postValue(ViewState(ViewState.Status.LOADING)) loadBooksUseCase.execute(null, { books -> val booksBinding = books.map { book -> BookConverter.fromData(book) } state.postValue( ViewState(ViewState.Status.SUCCESS, booksBinding) ) }, { e -> state.postValue( ViewState(ViewState.Status.ERROR, error = e) ) } ) } }
  • 46.
    class BookListFragment :BaseFragment() { private val viewModel: BookListViewModel private fun init() { ... lifecycle.addObserver(viewModel) } ...
  • 47.
  • 48.
    UI (app) Navigation API •Serve para implementar o conceito de “Single Activity” com múltiplos Fragments/Views. • Centraliza a lógica de navegação da aplicação. • Configuração direta com componentes de UI (ActionBar, Menu, BottomNav, …) • Simplifica a passagem de parâmetros.
  • 50.
    <navigation ... android:id="@+id/main_graph" app:startDestination="@id/listBooks"> <fragment android:id="@+id/listBooks" android:name="dominando.android.livros.BookListFragment" tools:layout="@layout/fragment_book_list"> ... <action android:id="@+id/action_list_to_details" app:destination="@id/bookDetails"/> </fragment> <fragment android:id="@+id/bookDetails" android:name="dominando.android.livros.BookDetailsFragment" tools:layout="@layout/fragment_book_details"> ... <argument android:name="book" app:argType="dominando.android.presentation.binding.Book" app:nullable="true" /> </fragment> ... </navigation>
  • 51.
  • 52.
    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() } }
  • 53.
    // 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
  • 54.
  • 55.
  • 56.
    class MyWork(ctx: Context,params: WorkerParameters): Worker(ctx, params) { override fun doWork(): Result { val firstName = inputData.getString(PARAM_FIRST_NAME) outputData = Data.Builder() .putString(PARAM_NAME, "$firstName Glauber") .putInt(PARAM_AGE, 35) .putLong(PARAM_TIME, System.currentTimeMillis()) .build() return Result.SUCCESS } companion object { const val PARAM_FIRST_NAME = "first_name" const val PARAM_NAME = "name" const val PARAM_AGE = "age" const val PARAM_TIME = "time" } }
  • 57.
    val input =Data.Builder() .putString(MyWork.PARAM_FIRST_NAME, "Nelson") .build() // Uma vez val request = OneTimeWorkRequest.Builder(MyWork::class.java) .setInputData(input) .build() // ou Periódico val request = PeriodicWorkRequest.Builder( MyWork::class.java, 5, TimeUnit.MINUTES) .setInputData(input) .build() observeAndEnqueue(request)
  • 58.
    private val wm= WorkManager.getInstance() private fun observeAndEnqueue(request: WorkRequest) { wm.enqueue(request) workId = request.id wm.getWorkInfoByIdLiveData(request.id) .observe(this, Observer { status -> txtStatus.text = when (status?.state) { WorkInfo.State.ENQUEUED -> "Enfileirado" WorkInfo.State.BLOCKED -> "Bloqueado" WorkInfo.State.CANCELLED -> "Cancelado" WorkInfo.State.RUNNING -> "Executando" WorkInfo.State.SUCCEEDED -> "Sucesso" WorkInfo.State.FAILED -> "Falhou" else -> "Indefinido" } txtOutput.text = status?.outputData?.run { getString(MyWork.PARAM_NAME) + getInt(MyWork.PARAM_AGE, 0) + getLong(MyWork.PARAM_TIME, 0) } }) }
  • 59.
    // Cancelando workId?.let {uuid -> wm.cancelWorkById(uuid) } wm.cancelAllWork()
  • 60.
    val constraints =Constraints.Builder() .setRequiredNetworkType(NetworkType.NOT_REQUIRED) .setRequiresBatteryNotLow(false) .setRequiresCharging(false) .setRequiresStorageNotLow(false) .build() val request = OneTimeWorkRequest.Builder(MyWork::class.java) .setConstraints(constraints) .build()
  • 61.
    val request0 =OneTimeWorkRequest.Builder(MyWork::class.java).build() val request1 = OneTimeWorkRequest.Builder(Worker1::class.java).build() val request2 = OneTimeWorkRequest.Builder(Worker2::class.java).build() val request3 = OneTimeWorkRequest.Builder(Worker3::class.java).build() wm.beginWith(request0) .then(listOf(request1, request2)) .then(request3) .enqueue()
  • 62.
  • 65.
    class CharactersDataSourceFactory( private valcompositeDisposable: CompositeDisposable, private val marvelApi: MarvelApi ) : DataSource.Factory<Int, Character>() { override fun create(): DataSource<Int, Character> { return CharactersDataSource(marvelApi, compositeDisposable) } }
  • 66.
    class CharactersDataSource( private valmarvelApi: MarvelApi, private val compositeDisposable: CompositeDisposable ) : PageKeyedDataSource<Int, Character>() { // PositionalDataSource<Character> // ItemKeyedDataSource<Int,Character> override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, Character>) { callback?.onResult(response.data.results, null, 1) } override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Character>) { callback?.onResult(response.data.results, params.key + 1) } override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, Character>) { callback?.onResult(response.data.results, params.key - 1) } }
  • 68.
    class CharactersViewModel :ViewModel() { var characterList: Observable<PagedList<Character>> private val compositeDisposable = CompositeDisposable() private val sourceFactory: CharactersDataSourceFactory init { sourceFactory = CharactersDataSourceFactory( compositeDisposable, MarvelApi.getService() ) val config = PagedList.Config.Builder() .setPageSize(20) .setInitialLoadSizeHint(40) .setPrefetchDistance(10) .setEnablePlaceholders(false) .build() characterList = RxPagedListBuilder(sourceFactory, config) .setFetchScheduler(Schedulers.io()) .buildObservable() .cache() } ... }
  • 70.
    class CharactersAdapter() :PagedListAdapter<Character, CharactersAdapter.VH>(CharacterDiffCallback) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH { val view = LayoutInflater.from(parent.context).inflate(R.layout.item_character, parent, false) return VH(view) } override fun onBindViewHolder(holder: VH, position: Int) { val character = getItem(position) holder.txtName.text = character?.name holder.imgThumbnail.load(character?.thumbnail?.path) } class VH(itemView: View) : RecyclerView.ViewHolder(itemView) { val imgThumbnail = itemView.imgThumbnail val txtName = itemView.txtName } companion object { val CharacterDiffCallback = object : DiffUtil.ItemCallback<Character>() { override fun areItemsTheSame(oldItem: Character, newItem: Character): Boolean { return oldItem.id == newItem.id } override fun areContentsTheSame(oldItem: Character, newItem: Character): Boolean { return oldItem == newItem } } } }
  • 71.
  • 72.
    • Você nãoprecisa usar todos esses recursos no mesmo app… • Na verdade, você não precisa usar nenhum! • Mais importante do que saber quando USAR é saber quando NÃO USAR 😉 • Mas é essencial conhecer tópicos, saber seus prós e contras e utilizá-los adequadamente 💡
  • 73.
  • 74.
    Referências • Android ArchitectureComponents
 https://developer.android.com/topic/libraries/architecture/ • LiveData beyond the ViewModel
 https://medium.com/androiddevelopers/livedata-beyond-the- viewmodel-reactive-patterns-using-transformations-and- mediatorlivedata-fda520ba00b7 • Android Architecture (Five Agency)
 https://five.agency/android-architecture-part-1-every-new- beginning-is-hard/ • Joe Birch (@hitherejoe) Course at Caster.io
 https://caster.io/courses/android-clean-architecture
  • 75.