Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Meetup_Architecture_Components.pdf

Rico
January 22, 2019

 Meetup_Architecture_Components.pdf

Rico

January 22, 2019
Tweet

More Decks by Rico

Other Decks in Technology

Transcript

  1. Architecture Components • Sammlung von Tools und Libraries • Sollen

    lose Kopplung und SOC vereinfachen • Robustere Apps durch Architecture Components • Lösung gängiger Probleme
  2. Architecture Components • Data Binding • LiveData • ViewModel •

    Room • (Paging) • Navigation • Workmanager
  3. LiveData • lifecycle-aware observable data holder • Benachrichtigt nur Observer

    im state STARTED oder RESUMED • LiveData und MutableLiveData • setValue() – MainThread • postValue()
  4. 2. Subscribe LiveData val lunchItem: LiveData<String> by lazy { MutableLiveData<String>()

    } lunchItem .observe(this, Observer<String> { lunchItem -> Log.i(this::class.simpleName, "Lunch item is $lunchItem") })
  5. 3. Set value val lunchItem: LiveData<String> by lazy { MutableLiveData<String>()

    } lunchItem .observe(this, Observer<String> { lunchItem -> Log.i(this::class.simpleName, "Lunch item is $lunchItem") }) lunchItem.value = "Sushi"
  6. Subclassing LiveData class LocationLiveData: LiveData<Location>() { private val locationManager =

    LocationManager() private val locationUpdateListener = { location: Location -> value = location } override fun onActive() { locationManager.registerLocationUpdates(locationUpdateListener) } override fun onInactive() { locationManager.unregisterLocationUpdates(locationUpdateListener) } }
  7. MediatorLiveData val lunchMenu: LiveData by lazy { val mediator =

    MediatorLiveData<String>() mediator.addSource(lunchItem) { item -> mediator.value = item } mediator.addSource(dessertItem) { item -> mediator.value = item } mediator } sampleViewModel.lunchMenu.observe(this, Observer { item -> Log.i(this::class.simpleName, "Received menu-item $item") }) sampleMenuButton.setOnClickListener { sampleViewModel.lunchItem.value = "Schnitzel" sampleViewModel.dessertItem.value = „Pudding" } 1. Erstellung einer MediatorLiveData 2. Observe MediatorLiveData 3. Änderungen in den Sources auch im Mediator verfügbar
  8. Transforming LiveData • Transformations.map(…) & Transformations.switchMap(…) • Transformationen werden lazy

    angewandt • map(LiveData<X> source, Function<X, Y> func) > Parameter func wird auf jedes emittierte Item angewandt • switchMap(LiveData<X> trigger, Function<X, LiveData<Y>> func) > Ähnlich map, returned LiveData
  9. ViewModel • Speichert und verwaltet UI-bezogene Daten • Kennt und

    beachtet Activity/Fragment lifecycle • Überlebt configuration-changes
  10. Viewmodel class MenuViewModel: ViewModel() { val lunchItems MutableLiveData<String> by lazy

    { MutableLiveData<String>() } } 1. ViewModel definieren class MenuActivity : AppCompatActivity() { lateinit var menuViewModel: MenuViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) menuViewModel = ViewModelProviders.of(this).get(MenuViewModel::class.java) menuViewModel .lunchItem .observe(this, Observer<String> { lunchItem -> Log.i(this::class.simpleName, "Lunch item is $lunchItem") }) } } 2. ViewModel beschaffen
  11. Shared ViewModels VM ermöglicht Kommunikation zwischen Fragments 1. Fragments erzeugen

    an die parent Activity gebundenes ViewModel 2. Fragments können über das ViewModel auf LiveData-Referenz zugreifen 3. Änderungen an LiveData durch .setValue() oder .postValue() werden an Subscriber propagiert val sharedViewModel = activity?.run { ViewModelProviders.of(this).get(MenuViewModel::class.java) }
  12. Recommended App Architecture: Data • Web • Bisher: Third-Party-Libs wie

    Retrofit • Local • Bisher: SQLite, Objectbox, ORMs, … • Empfehlung: Room Typische Datasources: Quelle: https://developer.android.com/jetpack/docs/guide
  13. Room Persistence Library • Abstraktion der SQLite APIs • Früher:

    Schreiben langer SQLite CREATE, DELETE queries • Heute: Annontationen an Entities > CREATE TABLE query wird generiert private const val SQL_CREATE_ENTRIES = "CREATE TABLE ${FeedEntry.TABLE_NAME} (" + "${BaseColumns._ID} INTEGER PRIMARY KEY," + "${FeedEntry.COLUMN_NAME_TITLE} TEXT," + "${FeedEntry.COLUMN_NAME_SUBTITLE} TEXT)" private const val SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS ${FeedEntry.TABLE_NAME}"
  14. Room • Drei wichtige Komponenten • Database - Haupteinstiegspunkt •

    Entity - Mapping von BO auf relationale Datenbank • DAO - Funktionalität zum Datenzugriff (Queries)
  15. Room Database • Muss mit @Database annontiert werden • Muss

    abstract und von „RoomDatabase“ abgeleitet sein • Enthält Liste der verwalteten Entities • Muss abstract Method enthalten, die DAO zurück liefert (wird von Room generiert) @Database( entities = [LunchItemDbEntity::class], version = 2, exportSchema = false ) @TypeConverters(DateTypeConverter::class) abstract class LunchDatabase : RoomDatabase() { abstract fun lunchItemDao(): LunchItemDao companion object { fun getInstance(context: Context): LunchDatabase = Room.databaseBuilder(context, LunchDatabase::class.java, "lunchdb") .build() } }
  16. Room - Entities @Entity data class LunchItemDbEntity( @PrimaryKey(autoGenerate = false)

    var id: Long = 0, var description: String, var offerDate: Date, var price: Float) : Serializable • Über @Entity-Annontation Parameter kann Entities-DB Repräsentation gesteuert werden • Indizes • TableNames • ForeignKeys
  17. Room - DAO @Dao interface LunchItemDao { @Insert(onConflict = OnConflictStrategy.REPLACE)

    fun insert(note: LunchItemDbEntity) @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertAll(note: List<LunchItemDbEntity>) @Query("SELECT id FROM LunchItemDbEntity WHERE offerDate >= :date ORDER BY offerDate LIMIT 1") fun getLunchIdOfDate(date: Long): Int } • Default Annotations für Insert, Delete, … • Queries können LiveData returnen • RxJava support (via extension)
  18. Data Binding • Objektzugriffe direkt im Layout • Zugriff auf

    Attribute und Funktionen • Effektiv in Verbindung mit LiveData • Generierte Binding-Class pro Layout
  19. Pagination: PagedList • PagedList verwaltet Items • Lädt Pages aus

    Datasource • Nutzt BoundaryCallback zum Nachladen aus externen Quellen
  20. Paging – PagedListAdapter class MenuSliderAdapter : PagedListAdapter<LunchItemEntity, LunchItemViewHolder>(diffCallback) { override

    fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LunchItemViewHolder { return LunchItemViewHolder(LayoutInflater.from(parent.context) .inflate(R.layout.item_menu_item, parent, false)) } override fun onBindViewHolder(holder: LunchItemViewHolder, position: Int) { val lunchItem = getItem(position) …. } companion object { private val diffCallback = object : DiffUtil.ItemCallback<LunchItemEntity>() { override fun areItemsTheSame(oldItem: LunchItemEntity, newItem: LunchItemEntity): Boolean = oldItem.id == newItem.id override fun areContentsTheSame(oldItem: LunchItemEntity, newItem: LunchItemEntity): Boolean = oldItem == newItem } } } // Setting Adapter and Layout Manager // ... menuViewModel.getLunchItemList() .observe(this, Observer { lunchItemList -> lunchItemList?.let { menuSliderAdapter.submitList(it) } }) PagedListAdapter Implementierung Setting PagedList
  21. Paging – Room als Datasource @Dao interface LunchItemDao { //

    ... @Query("SELECT * FROM LunchItemDbEntity ORDER BY offerDate ASC ") fun allLunchItems(): DataSource.Factory<Int, LunchItemDbEntity> // ... } • PagedList ist „Snapshot“-Sicht auf Daten • Items einer PagedList sind immutable • Bei Aktualisierung der Daten wird neues Datasource/PagedList Paar erzeugt, das alte invalidiert • Datasource.Factory erzeugt Datasources Query zur Erzeugung einer Datasource.Factory
  22. Paging – Room als Datasource class LunchItemRepositoryImpl(private val lunchItemDao: LunchItemDao,

    private val mapper: LunchItemDbEntityMapper) : LunchItemRepository { private val pageSize = 25 private val initialLoadSizeHint = 3 * pageSize private val prefetchDistance = 3 * pageSize override fun getLunchItems(startTime: Long): LiveData<PagedList<LunchItemEntity>> { val pagedListConfig = PagedList.Config.Builder() .setEnablePlaceholders(true) .setInitialLoadSizeHint(initialLoadSizeHint) .setPageSize(pageSize) .setPrefetchDistance(prefetchDistance) .build() return LivePagedListBuilder(lunchItemDao.allLunchItems().map(mapper::toDomain), pagedListConfig) .build() } } • Konfiguration der PagedList über PagedList.Config • LivePagedListBuilder erzeugt PagedList • Erzeugt neue Instanzen der Datasource mithilfe Datasource.Factory Woher kommt nun die PagedList?!
  23. Paging – BoundaryCallback class LunchItemBoundaryCallback( val cutoffDate: Long, val pageSize:

    Long ) : PagedList.BoundaryCallback<LunchItemEntity>(), KoinComponent, Parcelable { … override fun onZeroItemsLoaded() { super.onZeroItemsLoaded() // LOAD DATA FROM WEB AND PERSIST IN ROOM } override fun onItemAtEndLoaded(itemAtEnd: LunchItemEntity) { super.onItemAtEndLoaded(itemAtEnd) …. } override fun onItemAtFrontLoaded(itemAtFront: LunchItemEntity) { super.onItemAtFrontLoaded(itemAtFront) …. } … } Gebräuchlicher Anwendungsfall: Mehrstufige Datenzugriff über Netzwerk und Datenbank
  24. Paging – Custom DataSource • Problem: Viele unterschiedliche Paging APIs

    im Umlauf • PageKeyedDatasource • Für APIs mit doppelt verketteten Pages • ItemKeyedDataSource • Der Key der nächsten Page kann aus dem letzten bekannten Item abgeleitet werden • PositionalDataSource • Unterstützt APIs mit Offset-basiertem Paging
  25. Paging – Custom DataSource class LocalLunchItemDatasource( val lunchItemDao: LunchItemDao, val

    mapper: LunchItemDbEntityMapper, val start: Long ): ItemKeyedDataSource<Long, LunchItemEntity>(){ override fun loadInitial(params: LoadInitialParams<Long>, callback: LoadInitialCallback<LunchItemEntity>) { var initialKey = params.requestedInitialKey val entities = lunchItemDao.lunchItemPageInitial(initialKey, params.requestedLoadSize) callback.onResult(entities.map { mapper.toDomain(it) }) } override fun loadAfter(params: LoadParams<Long>, callback: LoadCallback<LunchItemEntity>) { val entities = lunchItemDao.lunchItemPageAfter(params.key, params.requestedLoadSize) callback.onResult(entities.map { mapper.toDomain(it) }) } override fun loadBefore(params: LoadParams<Long>, callback: LoadCallback<LunchItemEntity>) { val entities = lunchItemDao.lunchItemPageBefore(params.key, params.requestedLoadSize) callback.onResult(entities.map { mapper.toDomain(it) }) } override fun getKey(item: LunchItemEntity): Long { return item.offerDate.time } } Zusätzlich: Custom DataSourceFactory! PagedList.Config als Parameter!
  26. Paging – Datasource invalidation class CustomDataSourceFactory(val lunchItemDao: LunchItemDao, val mapper:

    LunchItemDbEntityMapper, val start: Long): DataSource.Factory<Long, LunchItemEntity>(){ private val database: LunchDatabase by inject() var observer: DataSourceTableObserver var tracker: InvalidationTracker init { observer = DataSourceTableObserver("LunchItemDbEntity") tracker = database.invalidationTracker tracker.addObserver(observer) } override fun create(): DataSource<Long, LunchItemEntity> { val source = LocalLunchItemDatasource(lunchItemDao, mapper, start) observer.setCurrentDataSource(source) return source } } class DataSourceTableObserver(tableName: String) : InvalidationTracker.Observer(tableName) { private var dataSource: DataSource<*, *>? = null override fun onInvalidated( tables: Set<String>) { if (dataSource != null) dataSource!!.invalidate() } fun setCurrentDataSource(source: DataSource<*, *>) { dataSource = source } }
  27. Paging – Fazit • Für einfache unidirektionale paged Lists ausreichend

    • Bei komplexeren Anforderungen viel Einarbeitung/Recherche nötig