$30 off During Our Annual Pro Sale. View Details »

From LiveData to Coroutines & Flow - Android Summit

Jossi Wolf
October 08, 2020

From LiveData to Coroutines & Flow - Android Summit

Slides from my talk from Android Summit 2020 about converting LiveData to Coroutines & Flow, with a small section about StateFlow.

Jossi Wolf

October 08, 2020
Tweet

More Decks by Jossi Wolf

Other Decks in Programming

Transcript

  1. Jossi Wolf
    @jossiwolf
    From LiveData to
    Coroutines Flow
    #session-chat-d1-s2-t1

    View Slide

  2. @jossiwolf
    @jossiwolf

    View Slide

  3. @jossiwolf
    Live Data, LiveData everywhere

    View Slide

  4. @jossiwolf
    ªġƛŸƦŊƴŸƞǛ
    ßŊġǕwŸĚġŢ
    mŊǔġ(öƴö
    ÇXmöǛġƞ
    XŭǔŸŞġƦ
    mŊǔġ(öƴö
    XŭǔŸŞġƦ

    View Slide

  5. @jossiwolf
    ”LiveData is an observable data
    holder class”
    - Android Developers Documentation

    View Slide

  6. @jossiwolf
    class HomeViewModel {

    val latestData: LiveData = MutableLiveData()

    }
    LiveData - an example

    View Slide

  7. @jossiwolf
    class HomeViewModel {

    val latestData: LiveData = MutableLiveData()

    fun refresh() {

    val data = fetchData()

    latestData.post(data)

    }

    }
    LiveData - an example

    View Slide

  8. @jossiwolf
    class HomeFragment: Fragment() {

    fun setupUI() {

    latestData.observe(viewLifecycleOwner) { data #->

    ##...

    }

    }

    }
    LiveData - an example

    View Slide

  9. @jossiwolf
    # LiveData is great!
    *when used properly

    View Slide

  10. @jossiwolf
    # LiveData &
    Execution Context

    View Slide

  11. @jossiwolf
    class SupermarketRepository {

    fun fetchOpeningHours(

    id: SupermarketId

    ): LiveData = ##...

    }
    Our Repository

    View Slide

  12. @jossiwolf
    class SupermarketRepository {

    fun fetchOpeningHours(

    id: SupermarketId

    ): LiveData = ##...

    }
    Our Repository

    View Slide

  13. @jossiwolf
    class HelpViewModel(

    private val supermarketRepo: SupermarketRepository

    ): ViewModel() {

    fun fetchSupermarketOpeningHours(id: SupermarketId) =

    supermarketRepo.fetchOpeningHours(id).map { openingHours #->

    openingHours.hours.filter { it #!= null }

    }

    }
    Our ViewModel

    View Slide

  14. @jossiwolf
    class HelpViewModel(

    private val supermarketRepo: SupermarketRepository

    ): ViewModel() {

    fun fetchSupermarketOpeningHours(id: SupermarketId) =

    supermarketRepo.fetchOpeningHours(id).map { openingHours #->

    openingHours.hours.filter { it #!= null }

    }

    }
    Our ViewModel

    View Slide

  15. @jossiwolf
    class HelpViewModel(

    private val supermarketRepo: SupermarketRepository

    ): ViewModel() {

    fun fetchSupermarketOpeningHours(id: SupermarketId) =

    supermarketRepo.fetchOpeningHours(id).map { openingHours #->

    openingHours.hours.filter { it #!= null }

    }

    }
    Our ViewModel

    View Slide

  16. @jossiwolf
    class HelpViewModel(

    private val supermarketRepo: SupermarketRepository

    ): ViewModel() {

    fun fetchSupermarketOpeningHours(id: SupermarketId) =

    supermarketRepo.fetchOpeningHours(id).map { openingHours #->

    openingHours.hours.filter { it #!= null }

    }

    }

    View Slide

  17. @jossiwolf
    class HelpViewModel(

    private val supermarketRepo: SupermarketRepository

    ): ViewModel() {

    fun fetchSupermarketOpeningHours(id: SupermarketId) =

    supermarketRepo.fetchOpeningHours(id).map { openingHours #->

    openingHours.hours.filter { it #!= null }

    }

    }

    View Slide

  18. @jossiwolf
    class HelpViewModel(

    private val supermarketRepo: SupermarketRepository

    ): ViewModel() {

    fun fetchSupermarketOpeningHours(id: SupermarketId) =

    supermarketRepo.fetchOpeningHours(id).map { openingHours #->

    openingHours.hours.filter { it #!= null }

    }

    }

    View Slide

  19. @jossiwolf
    LiveData map(

    LiveData source,

    Function mapFunction

    ) {

    MediatorLiveData result = new MediatorLiveData#<>();

    result.addSource(source, (x) #-> {

    result.setValue(mapFunction.apply(x));

    });

    return result;

    }
    cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/Transformations.java
    androidx.lifecycle.Transformations#map

    View Slide

  20. @jossiwolf
    LiveData map(

    LiveData source,

    Function mapFunction

    ) {

    MediatorLiveData result = new MediatorLiveData#<>();

    result.addSource(source, (x) #-> {

    result.setValue(mapFunction.apply(x));

    });

    return result;

    }
    cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/Transformations.java
    androidx.lifecycle.Transformations#map

    View Slide

  21. @jossiwolf
    LiveData map(

    LiveData source,

    Function mapFunction

    ) {

    MediatorLiveData result = new MediatorLiveData#<>();

    result.addSource(source, (x) #-> {

    result.setValue(mapFunction.apply(x));

    });

    return result;

    }
    cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/Transformations.java
    androidx.lifecycle.Transformations#map

    View Slide

  22. @jossiwolf
    LiveData map(

    LiveData source,

    Function mapFunction

    ) {

    MediatorLiveData result = new MediatorLiveData#<>();

    result.addSource(source, (x) #-> {

    result.setValue(mapFunction.apply(x));

    });

    return result;

    }
    cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/Transformations.java
    androidx.lifecycle.Transformations#map

    View Slide

  23. @jossiwolf
    LiveData map(

    LiveData source,

    Function mapFunction

    ) {

    MediatorLiveData result = new MediatorLiveData#<>();

    result.addSource(source, (x) #-> {

    result.setValue(mapFunction.apply(x));

    });

    return result;

    }
    cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/Transformations.java
    androidx.lifecycle.Transformations#map

    View Slide

  24. @jossiwolf
    ”The events are dispatched on the
    main thread.”
    - Android Developers Documentation

    View Slide

  25. @jossiwolf
    # LiveData Observers are always called
    on the main thread

    View Slide

  26. @jossiwolf
    # It’s easy to let things slide when using
    LiveData

    View Slide

  27. @jossiwolf
    Replace LiveData
    Make sure threading is done
    properly

    View Slide

  28. @jossiwolf
    Replace LiveData
    @jossiwolf

    View Slide

  29. @jossiwolf
    One-Shot Streams of Data

    View Slide

  30. @jossiwolf
    Coroutines!
    One-Shot Streams of Data

    View Slide

  31. @jossiwolf
    Coroutines! Flow
    One-Shot Streams of Data

    View Slide

  32. @jossiwolf
    # Converting to Coroutines

    View Slide

  33. @jossiwolf
    fun fetchOpeningHours(id: SupermarketId): LiveData {

    val liveData = MutableLiveData()

    val openingHours = poiService.downloadOpeningHours(poiType = SUPERMARKET, id).enqueue(

    object: Callback() {

    override fun onResponse(call: Call, response: Response) {

    liveData.postValue(response.body())

    }

    ##...

    }

    )

    liveData.postValue(openingHours)

    return liveData

    }
    Repository with LiveData + Callbacks

    View Slide

  34. @jossiwolf
    suspend fun fetchOpeningHours(id: SupermarketId): LiveData {

    val liveData = MutableLiveData()

    val openingHours = poiService.downloadOpeningHours(poiType = SUPERMARKET, id)

    liveData.postValue(openingHours)

    return liveData

    }
    Repository with LiveData + Coroutines

    View Slide

  35. @jossiwolf
    suspend fun fetchOpeningHours(id: SupermarketId) =

    poiService.downloadOpeningHours(poiType = SUPERMARKET, id)
    That method as Coroutine…

    View Slide

  36. @jossiwolf
    With Migration Helper
    suspend fun fetchOpeningHours(id: SupermarketId) =

    poiService.downloadOpeningHours(poiType = SUPERMARKET, id)

    @Deprecated

    fun fetchOpeningHoursAsLiveData(id: SupermarketId) =

    liveData {

    emit(fetchOpeningHours(id))

    }

    View Slide

  37. @jossiwolf
    With Migration Helper
    suspend fun fetchOpeningHours(id: SupermarketId) =

    poiService.downloadOpeningHours(poiType = SUPERMARKET, id)

    @Deprecated

    fun fetchOpeningHoursAsLiveData(id: SupermarketId) =

    liveData {

    emit(fetchOpeningHours(id))

    }

    View Slide

  38. @jossiwolf
    With Migration Helper
    suspend fun fetchOpeningHours(id: SupermarketId) =

    poiService.downloadOpeningHours(poiType = SUPERMARKET, id)

    @Deprecated

    fun fetchOpeningHoursAsLiveData(id: SupermarketId) =

    liveData {

    emit(fetchOpeningHours(id))

    }

    View Slide

  39. @jossiwolf
    With Migration Helper
    suspend fun fetchOpeningHours(id: SupermarketId) =

    poiService.downloadOpeningHours(poiType = SUPERMARKET, id)

    @Deprecated

    fun fetchOpeningHoursAsLiveData(id: SupermarketId) =

    liveData {

    emit(fetchOpeningHours(id))

    }

    View Slide

  40. @jossiwolf
    With Migration Helper
    suspend fun fetchOpeningHours(id: SupermarketId) =

    poiService.downloadOpeningHours(poiType = SUPERMARKET, id)

    @Deprecated

    fun fetchOpeningHoursAsLiveData(id: SupermarketId) =

    liveData(context = Dispatchers.IO) {

    emit(fetchOpeningHours(id))

    }

    View Slide

  41. @jossiwolf
    class HelpViewModel(

    private val repo: SupermarketRepository

    ): ViewModel() {

    fun fetchSupermarketOpeningHours(id: SupermarketId) =

    repo.fetchOpeningHours(id).map { openingHours #->

    openingHours.hours.filter { it #!= null }

    }

    }
    Our ViewModel

    View Slide

  42. @jossiwolf
    class HelpViewModel(

    private val repo: SupermarketRepository

    ): ViewModel() {

    fun fetchSupermarketOpeningHours(id: SupermarketId) =

    repo.fetchOpeningHoursAsLiveData(id).map { openingHours #->

    openingHours.hours.filter { it #!= null }

    }

    }
    Our ViewModel

    View Slide

  43. @jossiwolf
    class HelpViewModel(

    private val repo: SupermarketRepository

    ): ViewModel() {

    fun fetchSupermarketOpeningHours(id: SupermarketId) =

    repo.fetchOpeningHoursAsLiveData(id).map { openingHours #->

    openingHours.hours.filter { it #!= null }

    }

    }
    Our ViewModel

    View Slide

  44. @jossiwolf
    Our ViewModel
    class HelpViewModel(

    private val repo: SupermarketRepository

    ): ViewModel() {

    fun fetchSupermarketOpeningHours(id: SupermarketId) =

    repo.fetchOpeningHoursAsLiveData(id).map { openingHours #->

    openingHours.hours.filter { it #!= null }

    }

    }

    View Slide

  45. @jossiwolf
    Repository with Migration Helper
    suspend fun fetchOpeningHours(id: SupermarketId) =

    poiService.downloadOpeningHours(poiType = SUPERMARKET, id)

    @Deprecated

    fun fetchOpeningHoursAsLiveData(id: SupermarketId) =

    liveData(context = Dispatchers.IO) {

    emit(fetchOpeningHours(id))

    }

    View Slide

  46. @jossiwolf
    Our ViewModel
    class HelpViewModel(

    private val repo: SupermarketRepository

    ): ViewModel() {

    fun fetchSupermarketOpeningHours(id: SupermarketId) =

    repo.fetchOpeningHoursAsLiveData(id).map { openingHours #->

    openingHours.hours.filter { it #!= null }

    }

    }

    View Slide

  47. @jossiwolf
    Our ViewModel
    class HelpViewModel(

    private val repo: SupermarketRepository

    ): ViewModel() {

    fun fetchSupermarketOpeningHours(id: SupermarketId) =

    liveData(context = Dispatchers.IO) {

    val openingHours = repo.fetchOpeningHours(id)

    val filteredHours = openingHours.hours.filter { it #!= null }

    emit(filteredHours)

    }

    }

    View Slide

  48. @jossiwolf
    # Converting to Flow

    View Slide

  49. @jossiwolf
    suspend fun fetchSupermarketDetails(

    id: SupermarketId

    ): LiveData {

    val liveData = MutableLiveData()

    val cachedDetails = detailsCache[id]

    if (cachedDetails #!= null) liveData.postValue(cachedDetails)

    val freshDetails = poiService.downloadDetails(SUPERMARKET, id)

    liveData.postValue(freshDetails)

    return liveData

    }
    With LiveData

    View Slide

  50. @jossiwolf
    suspend fun fetchSupermarketDetails(

    id: SupermarketId

    ): LiveData {

    val liveData = MutableLiveData()

    val cachedDetails = detailsCache[id]

    if (cachedDetails #!= null) liveData.postValue(cachedDetails)

    val freshDetails = poiService.downloadDetails(SUPERMARKET, id)

    liveData.postValue(freshDetails)

    return liveData

    }
    With LiveData

    View Slide

  51. @jossiwolf
    suspend fun fetchSupermarketDetails(

    id: SupermarketId

    ): LiveData {

    val liveData = MutableLiveData()

    val cachedDetails = detailsCache[id]

    if (cachedDetails #!= null) liveData.postValue(cachedDetails)

    val freshDetails = poiService.downloadDetails(SUPERMARKET, id)

    liveData.postValue(freshDetails)

    return liveData

    }
    With LiveData

    View Slide

  52. @jossiwolf
    fun fetchSupermarketDetails(

    id: SupermarketId

    ): Flow = flow {

    val cachedDetails = detailsCache[id]

    if (cachedDetails #!= null) emit(cachedDetails)

    val freshDetails = poiService.downloadDetails(SUPERMARKET, id)

    emit(freshDetails)

    }
    With Flow

    View Slide

  53. @jossiwolf
    fun fetchSupermarketDetails(

    id: SupermarketId

    ): Flow = flow {

    val cachedDetails = detailsCache[id]

    if (cachedDetails #!= null) emit(cachedDetails)

    val freshDetails = poiService.downloadDetails(SUPERMARKET, id)

    emit(freshDetails)

    }
    With Flow

    View Slide

  54. @jossiwolf
    fun fetchSupermarketDetails(

    id: SupermarketId

    ): Flow = flow {

    val cachedDetails = detailsCache[id]

    if (cachedDetails #!= null) emit(cachedDetails)

    val freshDetails = poiService.downloadDetails(SUPERMARKET, id)

    emit(freshDetails)

    }
    With Flow

    View Slide

  55. @jossiwolf
    fun fetchSupermarketDetails(

    id: SupermarketId

    ): Flow = flow {

    val cachedDetails = detailsCache[id]

    if (cachedDetails #!= null) emit(cachedDetails)

    val freshDetails = poiService.downloadDetails(SUPERMARKET, id)

    emit(freshDetails)

    }
    With Flow

    View Slide

  56. @jossiwolf
    With Flow + Migration Helper
    fun fetchSupermarketDetails(

    id: SupermarketId

    ): Flow = flow { … }

    @Deprecated("Please use Flows directly.")

    fun fetchSupermarketDetailsAsLiveData(

    id: SupermarketId

    ): LiveData =

    fetchSupermarketDetails(id).asLiveData()

    View Slide

  57. @jossiwolf
    With Flow + Migration Helper
    fun fetchSupermarketDetails(

    id: SupermarketId

    ): Flow = flow { … }

    @Deprecated("Please use Flows directly.")

    fun fetchSupermarketDetailsAsLiveData(

    id: SupermarketId

    ): LiveData =

    fetchSupermarketDetails(id).asLiveData(Dispatchers.IO)

    View Slide

  58. @jossiwolf
    With Flow + Migration Helper
    fun fetchSupermarketDetails(

    id: SupermarketId

    ): Flow = flow { … }

    @Deprecated("Please use Flows directly.")

    fun fetchSupermarketDetailsAsLiveData(

    id: SupermarketId

    ): LiveData =

    fetchSupermarketDetails(id).asLiveData(Dispatchers.IO)

    View Slide

  59. @jossiwolf
    With Flow + Migration Helper
    fun fetchSupermarketDetails(

    id: SupermarketId

    ): Flow = flow { … }

    @Deprecated("Please use Flows directly.")

    fun fetchSupermarketDetailsAsLiveData(

    id: SupermarketId

    ): LiveData =

    fetchSupermarketDetails(id).asLiveData(Dispatchers.IO)

    View Slide

  60. @jossiwolf
    # StateFlow

    View Slide

  61. @jossiwolf
    “A Flow that represents a read-only
    state with a single updatable data
    value that emits updates to the value
    to its collectors.”
    - StateFlow Documentation
    # StateFlow

    View Slide

  62. @jossiwolf
    Our ViewModel
    class HelpViewModel(

    private val repo: SupermarketRepository

    ): ViewModel() {

    fun fetchSupermarketOpeningHours(id: SupermarketId)

    : LiveData

    }

    View Slide

  63. @jossiwolf
    Our ViewModel
    class HelpViewModel(

    private val repo: SupermarketRepository

    ): ViewModel() {

    fun fetchSupermarketOpeningHours(id: SupermarketId)

    : Unit

    }

    View Slide

  64. @jossiwolf
    Our ViewModel
    class HelpViewModel(…): ViewModel() {

    val _openingHoursState = MutableStateFlow(OpeningHoursState.Empty)

    val openingHoursState: StateFlow

    = _openingHoursState

    fun fetchSupermarketOpeningHours(id: SupermarketId) {

    viewModelScope.launch(Dispatchers.IO) {

    val openingHours = repo.fetchOpeningHours()

    _openingHoursState.value = openingHours

    }

    }

    }

    View Slide

  65. @jossiwolf
    Our ViewModel
    class HelpViewModel(…): ViewModel() {

    val _openingHoursState = MutableStateFlow(OpeningHoursState.Empty)

    val openingHoursState: StateFlow

    = _openingHoursState

    fun fetchSupermarketOpeningHours(id: SupermarketId) {

    viewModelScope.launch(Dispatchers.IO) {

    val openingHours = repo.fetchOpeningHours()

    _openingHoursState.value = openingHours

    }

    }

    }

    View Slide

  66. @jossiwolf
    Our ViewModel
    class HelpViewModel(…): ViewModel() {

    val _openingHoursState = MutableStateFlow(OpeningHoursState.Empty)

    val openingHoursState: StateFlow

    = _openingHoursState

    fun fetchSupermarketOpeningHours(id: SupermarketId) {

    viewModelScope.launch(Dispatchers.IO) {

    val openingHours = repo.fetchOpeningHours()

    _openingHoursState.value = openingHours

    }

    }

    }

    View Slide

  67. @jossiwolf
    Our UI Layer
    fun onViewCreated(#..) {

    viewModel.openingHoursState

    .onEach(#::renderOpeningHours)

    .launchIn(lifecycleScope)

    }

    View Slide

  68. @jossiwolf
    -Single API instead of two APIs
    Why StateFlow?

    View Slide

  69. @jossiwolf
    All methods of data flow are thread-
    safe and can be safely invoked from
    concurrent coroutines without
    external synchronization.
    - StateFlow Documentation

    View Slide

  70. @jossiwolf
    -Single API instead of two APIs
    -More control over execution context
    Why StateFlow?

    View Slide

  71. @jossiwolf
    -Single API instead of two APIs
    -More control over execution context
    -Integrates nicely with Jetpack Compose
    Why StateFlow?

    View Slide

  72. @jossiwolf
    -Still experimental
    -Things are a bit unclear :)
    StateFlow Drawbacks

    View Slide

  73. @jossiwolf
    - developer.android.com/topic/libraries/architecture/livedata
    - proandroiddev.com/dont-use-livedata-in- repositories-f3bebe502ed3
    - medium.com/@elizarov/execution-context-of-kotlin-flows-b8c151c9309b
    - https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/
    kotlinx.coroutines.flow/-state-flow/
    Resources

    View Slide

  74. @jossiwolf

    View Slide

  75. Jossi Wolf
    @jossiwolf
    From LiveData to
    Coroutines Flow

    View Slide