Get Class By String Name In Android To Avoid Repeating Myself - android

So in my code, I'm getting a list of strings, which correspond to movie genres in an enum. I'm then getting a metadata file, and going through each of those genres, seeing if the date the server updated that genre is newer than the date I retrieved those movies and stored them in the database. Long story short, the metadata file has classes for each of the genres, and I'm doing a TON of repeating myself, simply for the fact that I can't think of how to programtically let my app know that, for instance, if you're evaluating the genre ACTION you should look at the member variable metadata.action. Here's my code, I've truncated it but there are a couple dozen genres.
TwMetadata
data class TwMetadata (
#SerializedName("action") var action : Action,
#SerializedName("adventure") var adventure : Adventure,
#SerializedName("animation") var animation : Animation,
#SerializedName("comedy") var comedy : Comedy,
)
In the following code, I've taken the list of strings I've pulled from the server, converted them into genre enums, and am checking the date last updated in the sharedpreferences metadata file (spObj), vs the date last updated in the newly pulled metadata file(metadata.value). If there is new data, I'm changing the date in the sharedpreferences metadata file, which I commit at the end of this completable. Then I'm sending it to a function which either pulls the data from the database or the web, depending on the value of the boolean i'm sending it. Ideally I'd be able to get rid of this entire 'when' block and just run a single function on each genre, but for that to work they'd need to know what class they correspond to in the metadata file.
private fun checkGenreDate(genreList: MutableList<GENRE>) : Completable {
return Completable.create { emitter ->
var getNew = json.isNullOrEmpty()
genreList.forEach {
when (it) {
GENRE.NULL -> {
}
GENRE.ACTION -> {
if (spObj.action.updated < metadata.value!!.action.updated) {
getNew = true
spObj.action = metadata.value!!.action
}
loadFromTwApi(it, getNew)
}
GENRE.ADVENTURE -> {
if (spObj.adventure.updated < metadata.value!!.adventure.updated) {
getNew = true
spObj.adventure = metadata.value!!.adventure
}
loadFromTwApi(it, getNew)
}
GENRE.ANIMATION -> {
if (spObj.animation.updated < metadata.value!!.animation.updated) {
getNew = true
spObj.animation = metadata.value!!.animation
}
loadFromTwApi(it, getNew)
}
GENRE.COMEDY -> {
if (spObj.comedy.updated < metadata.value!!.comedy.updated) {
getNew = true
spObj.comedy = metadata.value!!.comedy
}
loadFromTwApi(it, getNew)
}
GENRE.RANDOM -> {
}
GENRE.THEATER -> {
loadTheaterData()
}
}
}
emitter.onComplete()
}
}
my enum class
enum class GENRE {
NULL,
ACTION,
ADVENTURE,
ANIMATION,
COMEDY,
RANDOM,
THEATER;
companion object {
fun getGenreByName(name: String) = try {
(name.toUpperCase(Locale.getDefault()))
} catch (e: IllegalArgumentException) {
null
}
}

I suggest a generic sealed class with abstract metadata get and set methods, and a static lookup method included on the sealed class. Pretty much Kotlin magic for turning enums into a class hierarchy so you can both share and enforce functionality amongst them.
So for just Action and Adventure, here's a function updateAllGenres which will update all the metadata.
fun updateAllGenres( spObj: TwMetadata, metadata: MetadataFile ) {
Genre.forEach {
it.maybeUpdateSharedMetadata( spObj, metadata )
}
}
sealed class Genre < T : GenreMetadata > {
companion object {
fun forEach( callback: ( Genre < * > ) -> Unit ) {
Genre::class.sealedSubclasses.forEach {
callback( it.objectInstance as Genre < * > )
}
}
}
fun maybeUpdateSharedMetadata( spObj: TwMetadata, metadata: MetadataFile ): Boolean {
val genreMetadataInFile = getGenreMetadata( metadata.value!! )
return getGenreMetadata( spObj ).updated < genreMetadataInFile.updated
.also { setGenreMetadataInDictionary( spObj, genreMetadataInFile )
}
}
protected abstract fun getGenreMetadata( metadata: TwMetadata ): T
protected abstract fun setGenreMetadataInDictionary( metadata: TwMetadata, genreMetadata: T )
}
object ActionGenre : Genre < Action >() {
override fun getGenreMetadata( metadata: TwMetadata ): Action {
return metadata.action
}
override fun setGenreMetadataInDictionary( metadata: TwMetadata, genreMetadata: Action ) {
metadata.action = genreMetadata
}
}
object AdventureGenre : Genre < Adventure >() {
override fun getGenreMetadata( metadata: TwMetadata ): Adventure {
return metadata.adventure
}
override fun setGenreMetadataInDictionary( metadata: TwMetadata, genreMetadata: Adventure ) {
metadata.adventure = genreMetadata
}
}
A couple more tweaks are needed to get exactly your function, but I'm sure you get the idea. Also, you might want to go further than this and look at combining the sealed subclasses and the classes Action, Adventure etc that hold the metadata, but that's a bit more work.
For reference the interfaces in the above are:
interface Action : GenreMetadata {}
interface Adventure : GenreMetadata {}
interface GenreMetadata {
val updated: Long
}
interface TwMetadata {
var action: Action
var adventure: Adventure
}
interface MetadataFile {
val value: TwMetadata?
}

Related

Refresh Data in ViewModel when Navigating back - Android(Kotlin)

I have the following setup.
I have a screen with a list of items (PlantsScreen). When clicking on an item from the list I will be navigated to another screen (AddEditPlantScreen). After editing and saving the item and navigating back to the listScreen, I want to show the updated list of items. But the list is not displaying the updated list but the list before the edit of the item.
In order to have a single source of truth, I am fetching the data from a node.js Back-End and then saving it to the local repository (Room). I think I need to refresh the state in the ViewModel to fetch the updated list from my repository.
I know I can use a Job to do this, but it throws me an error. Is this the correct approach when returning a Flow?
If yes, how can I achieve this.
If not, what alternative approach do I have?
plantsListViewModel.kt
private val _state = mutableStateOf<PlantsState>(PlantsState())
val state: State<PlantsState> = _state
init {
getPlants(true, "")
}
private fun getPlants(fetchFromBackend: Boolean, query: String) {
viewModelScope.launch {
plantRepository.getPlants(fetchFromBackend, query)
.collect { result ->
when (result) {
is Resource.Success -> {
result.data?.let { plants ->
_state.value = state.value.copy(
plants = plants,
)
}
}
}
}
}
}
Here is my repository where I fetch the items in the list from.
// plantsRepository.kt
override suspend fun getPlants(
fetchFromBackend: Boolean,
query: String
): Flow<Resource<List<Plant>>> {
return flow {
emit(Resource.Loading(true))
val localPlants = dao.searchPlants(query)
emit(
Resource.Success(
data = localPlants.map { it.toPlant() },
)
)
val isDbEmpty = localPlants.isEmpty() && query.isBlank()
val shouldLoadFromCache = !isDbEmpty && !fetchFromBackend
if (shouldLoadFromCache) {
emit(Resource.Loading(false))
return#flow
}
val response = plantApi.getPlants().plants
dao.clearPlants()
dao.insertPlants(
response.map { it.toPlantEntity() }
)
emit(Resource.Success(
data = dao.searchPlants("").map { it.toPlant() }
))
emit(Resource.Loading(false))
}
}
The full code for reference can be found here:
https://gitlab.com/fiehra/plants
Thank you!
You actually have two sources of truth: One is the room database, the other the _state object in the view model.
To reduce this to a single source of truth you need to move the collection of the flow to the compose function where the data is needed. You will do this using the extension function StateFlow.collectAsStateWithLifecycle() from the artifact androidx.lifecycle:lifecycle-runtime-compose. This will automatically subscribe and unsubscribe the flow when your composable enters and leaves the composition.
Since you want the business logic to stay in the view model you have to apply it before the flow is collected. The idea is to only transform the flow in the view model:
class PlantsViewModel {
private var fetchFromBackend: Boolean by mutableStateOf(true)
private var query: String by mutableStateOf("")
#OptIn(ExperimentalCoroutinesApi::class)
val state: StateFlow<PlantsState> =
snapshotFlow { fetchFromBackend to query }
.flatMapLatest { plantRepository.getPlants(it.first, it.second) }
.mapLatest(PlantsState::of)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = PlantsState.Loading,
)
// ...
}
If you want other values for fetchFromBackend and query you just need to update the variables; the flow will automatically recalculate the state object. It can be as simple as just calling something like this:
fun requestPlant(fetchFromBackend: Boolean, query: String) {
this.fetchFromBackend = fetchFromBackend
this.query = query
}
The logic to create a PlantsState from a result can then be done somewhere else in the view model. Replace your PlantsViewModel.getPlants() with this and place it at file level outside of the PlantsViewModel class:
private fun PlantsState.Companion.of(result: Resource<List<Plant>>): PlantsState = when (result) {
is Resource.Success -> {
result.data?.let { plants ->
PlantsState.Success(
plants = plants,
)
} ?: TODO("handle case where result.data is null")
}
is Resource.Error -> {
PlantsState.Error("an error occurred")
}
is Resource.Loading -> {
PlantsState.Loading
}
}
With the PlantsState class replaced by this:
sealed interface PlantsState {
object Loading : PlantsState
data class Success(
val plants: List<Plant> = emptyList(),
val plantOrder: PlantOrder = PlantOrder.Name(OrderType.Descending),
val isOrderSectionVisible: Boolean = false,
) : PlantsState
data class Error(
val error: String,
) : PlantsState
companion object
}
Then, wherever you need the state (in PlantsScreen f.e.), you can get a state object with
val state by viewModel.state.collectAsStateWithLifecycle()
Thanks to kotlin flows state will always contain the most current data from the room database, and thanks to the compose magic your composables will always update when anything in the state object updates, so that you really only have one single source of truth.
Additionally:
PlantRepository.getPlants() should not be marked as a suspend function because it just creates a flow and won't block; long running data retrieval will be done in the collector.
You will need to manually import androidx.compose.runtime.getValue and the androidx.compose.runtime.setValue for some of the delegates to work.
After #Leviathan was able to point me in the right direction i refactored my code by changing the return types of my repository functions, implementing use cases and returning a Flow<List<Plant>> instead of Flow<Resource<List<Plant>>> for simplicity purposes.
Further removed the suspend marker of the functions in the PlantDao.kt and PlantRepository.kt as pointed out by Leviathan.
// PlantRepositoryImplementation.kt
override fun getPlants(
fetchFromBackend: Boolean,
query: String
): Flow<List<Plant>> {
return flow {
if (fetchFromBackend) {
val response = plantApi.getPlants().plants
dao.clearPlants()
dao.insertPlants(
response.map { it.toPlantEntity() }
)
val localPlants = dao.searchPlants(query)
localPlants.collect { plants ->
emit(plants.map { it.toPlant() })
return#collect
}
} else {
val localPlants = dao.searchPlants(query)
localPlants.collect { plants ->
emit(plants.map { it.toPlant() })
return#collect
}
}
}
}
I started using a Job and GetPlants usecase in my viewModel like this:
// PlantsViewModel.kt
private fun getPlants(plantOrder: PlantOrder, fetchFromBackend: Boolean, query: String) {
getPlantsJob?.cancel()
getPlantsJob = plantUseCases.getPlants(plantOrder, fetchFromBackend, query)
.onEach { plants ->
_state.value = state.value.copy(
plants = plants,
plantOrder = plantOrder
)
}.launchIn(viewModelScope)
I also had to remove the suspend in the PlantDao.kt
// PlantDao.kt
fun searchPlants(query: String): Flow<List<PlantEntity>>
This is the code for my GetPlants usecase:
// GetPlantsUsecase.kt
class GetPlants
(
private val repository: PlantRepository,
) {
operator fun invoke(
plantOrder: PlantOrder = PlantOrder.Name(OrderType.Descending),
fetchFromBackend: Boolean,
query: String
): Flow<List<Plant>> {
return repository.getPlants(fetchFromBackend, query).map { plants ->
when (plantOrder.orderType) {
is OrderType.Ascending -> {
// logic for sorting
}
}
is OrderType.Descending -> {
// logic for sorting
}
}
}
}
}

Paging 3 Compose :insertSeparators not working as expected

I'm trying to insert separators to my list using the paging 3 compose library however, insertSeparators doesn't seem to indicate when we are at the beginning or end. My expectations are that before will be null at the beginning while after will be null at the end of the list. But it's never null thus hard to know when we are at the beginning or end. Here is the code:
private val filterPreferences =
MutableStateFlow(HomePreferences.FilterPreferences())
val games: Flow<PagingData<GameModel>> = filterPreferences.flatMapLatest {
useCase.execute(it)
}.map { pagingData ->
pagingData.map { GameModel.GameItem(it) }
}.map {
it.insertSeparators {before,after->
if (after == null) {
return#insertSeparators null
}
if (before == null) {
Log.i(TAG, "before is null: ") // never reach here
return#insertSeparators GameModel.SeparatorItem("title")
}
if(condition) {
GameModel.SeparatorItem("title")
}
else null
}
}
.cachedIn(viewModelScope)
GamesUseCase
class GamesUseCase #Inject constructor(
private val executionThread: PostExecutionThread,
private val repo: GamesRepo,
) : FlowUseCase<HomePreferences, PagingData<Game>>() {
override val dispatcher: CoroutineDispatcher
get() = executionThread.io
override fun execute(params: HomePreferences?): Flow<PagingData<Game>> {
val preferences = params as HomePreferences.FilterPreferences
preferences.apply {
return repo.fetchGames(query,
parentPlatforms,
platforms,
stores,
developers,
genres,
tags)
}
}
}
FlowUseCase
abstract class FlowUseCase<in Params, out T>() {
abstract val dispatcher: CoroutineDispatcher
abstract fun execute(params: Params? = null): Flow<T>
operator fun invoke(params: Params? = null) = execute(params).flowOn(dispatcher)
}
Here is the dependency :
object Pagination {
object Version {
const val pagingCompose = "1.0.0-alpha14"
}
const val pagingCompose = "androidx.paging:paging-compose:${Version.pagingCompose}"
}
I'm assuming that filterPreferences gives you Flow of some preference and useCase.execute returns Flow<PagingData<Model>>, correct?
I believe that the problem is in usage of flatMapLatest - it mixes page events of multiple useCase.execute calls together.
You should do something like this:
val games: Flow<Flow<PagingData<GameModel>>> = filterPreferences.mapLatest {
useCase.execute(it)
}.mapLatest {
it.map { pagingData -> pagingData.map { GameModel.GameItem(it) } }
}.mapLatest {
it.map { pagingData ->
pagingData.insertSeparators { before, after -> ... }
} // .cachedIn(viewModelScope)
}
This same structure works for us very well. I'm only not sure how cachedIn will work here, we are using a different caching mechanism, but you can try.

How to create calculated properties in data class with GSON implemented?

Background: GSON, Kotlin, Retrofit
I am writing a restaurant app. In the home page, the user is able to load a list of restaurant brands. Each brand can have up to 3 cuisine types, the first one is non-null and the next two are nullable. Each cuisine type is within the CuisineType enum class.
What I would like to do is to create a joined string like this:
cuisineType1.title + cuisineType2?.title + cuisineType3?.title = combinedCuisines. This can make all cuisines shown within a textView in Chinese. In order to do this I created a helper class. In this helper class, if the CuisineType from Brand cannot map any of the enum members, it will display the raw name from the Brand JSON(incase of server error). I tried the three solutions commented out below and non of them work. Much help would be appreciated. Thanks in advance!
data class Brand(
#SerializedName("id")
val id: Int,
#SerializedName("name_en")
val nameEN: String?,
#SerializedName("cuisine_1")
val cuisineType1: String,
#SerializedName("cuisine_2")
val cuisineType2: String?,
#SerializedName("cuisine_3")
val cuisineType3: String?,
/*Solution 1(not working):
val combinedCuisines = CombineCuisineHelper.combineCuisines(cuisineType1, cuisineType2, cuisineType3)
***java.lang.IllegalArgumentException: Unable to create converter for class
*/
/*Solution 2(not working):
#Transient
val combinedCuisines = CombineCuisineHelper.combineCuisines(cuisineType1, cuisineType2, cuisineType3)
***combinedCuisines = null after network call in fragment
*/
) {
/* Solution 3(not working):
val combinedCuisines: String
get() = CombineCuisineHelper.combineCuisines(cuisineType1, cuisineType2, cuisineType3)
***problem with GSON, I can only map the #SerializedName from the Cuisine enum class and will only run the illegal argument solution from the CombineCuisineHelper. For example, get hong_kong_style from the JSON brand but it will not convert to HongKongStyle and map to its title.
*/
}
//It should be a long list but I shortened it.
enum class CuisineType {
#SerializedName("chinese")
Chinese,
#SerializedName("hong_kong_style")
HongKongStyle,
#SerializedName("cantonese")
Cantonese,
val title: Double
get() {
return when (this) {
Chinese -> "中菜"
HongKongStyle -> "港式"
Cantonese -> "粵式"
}
class CombineCuisineHelper {
companion object {
fun combineCuisines(cuisineSubtype1: String, cuisineSubtype2: String?, cuisineSubtype3: String?): String {
val combinedSubtypes = ArrayList<String?>()
combinedSubtypes += try {
CuisineSubtype.valueOf(cuisineSubtype1).title
} catch (e: IllegalArgumentException) {
cuisineSubtype1
}
if (cuisineSubtype2 != null) {
combinedSubtypes += try {
CuisineSubtype.valueOf(cuisineSubtype2).title
} catch (e: IllegalArgumentException) {
cuisineSubtype2
}
}
if (cuisineSubtype3 != null) {
combinedSubtypes += try {
CuisineSubtype.valueOf(cuisineSubtype3).title
} catch (e: IllegalArgumentException) {
cuisineSubtype3
}
}
}
The first and second solutions are not good, because the data might not be ready at the initialization time. The third solution is the one we can continue on:
val combinedCuisines: String
get() = CombineCuisineHelper.combineCuisines(cuisineType1, cuisineType2, cuisineType3)
The SerializedNames are useless for enum constants and won't work for you as you are expecting. So the valueOf method for enum won't find a value for literals like "hong_kong_style" and will throw an exception.
You can create your own helper method in your enum class like this:
enum class CuisineType {
Chinese,
HongKongStyle,
Cantonese;
val title: String
get() {
return when (this) {
Chinese -> "中菜"
HongKongStyle -> "港式"
Cantonese -> "粵式"
}
}
companion object {
//Note this helper method which manually maps string values to enum constants:
fun enumValue(title: String): CuisineType = when (title) {
"chinese" -> Chinese
"hong_kong_style" -> HongKongStyle
"cantonese" -> Cantonese
else -> throw IllegalArgumentException("Unknown cuisine type $title")
}
}
}
And then use this new method instead of the enum's own valueOf method:
val combinedSubtypes = ArrayList<String?>()
combinedSubtypes += try {
CuisineType.enumValue(cuisineSubtype1).title
} catch (e: IllegalArgumentException) {
cuisineSubtype1
}
//continued...

How To Filter Retrofit2 Json Response To Keep Only Certain Elements In ArrayList

I'm new to kotlin so this maybe a very easy issue to resolve.
What I'm trying to do is filter the json response that I receive using Retrofit2 before I display the images in a grid with a RecyclerView.
instagram.com/explore/tags/{hashtag}/?__a=1&max_id= Using Retrofit2 I'm able to get the data response fine and also display the given url images in a RecyclerView.
I have not been successful in using the filter, map, loops and conditions to remove elements from the Arraylist. I do not understand these to the fullest extent but I have searched looking for solutions and those are what I came apon.
Interface
interface InstagramDataFetcher
{
#GET("tags/{tag}/?__a=1&max_id=")
fun getInstagramData(#Path("tag") hashtag: String) : Call <InstagramResponse>
}
Where I get my response from and also get StringIndexOutOfBoundsException
class InstagramFeedFragment : Fragment()
{
private fun onResponse()
{
val service = RestAPI.retrofitInstance?.create(InstagramDataFetcher::class.java)
val call = service?.getInstagramData("hashtag")
call?.enqueue(object : Callback<InstagramResponse>
{
override fun onFailure(call: Call<InstagramResponse>, t: Throwable)
{
Log.d("FEED", " $t")
}
override fun onResponse(
call: Call<InstagramResponse>, response: Response<InstagramResponse>
)
{
//for ((index, value) in data.withIndex())
if (response.isSuccessful)
{
var data: ArrayList<InstagramResponse.InstagramEdgesResponse>? = null
val body = response.body()
data = body!!.graphql.hashtag.edge_hashtag_to_media.edges
for ((index, value) in data.withIndex())
{
if(value.node.accessibility_caption[index].toString().contains("text") ||
value.node.accessibility_caption[index].toString().contains("person"))
{
data.drop(index)
}
}
recyclerView.adapter = InstagramGridAdapter(data, parentFragment!!.context!!)
}
}
})
}
}
This is my model class
data class InstagramResponse(val graphql: InstagramGraphqlResponse)
{
data class InstagramGraphqlResponse(val hashtag: InstagramHashtagResponse)
data class InstagramHashtagResponse(val edge_hashtag_to_media: InstagramHashtagToMediaResponse)
data class InstagramHashtagToMediaResponse(
val page_info: InstagramPageInfo,
val edges: ArrayList<InstagramEdgesResponse>
)
data class InstagramPageInfo(
val has_next_page: Boolean,
val end_cursor: String
)
data class InstagramEdgesResponse(val node: InstagramNodeResponse)
data class InstagramNodeResponse(
val __typename: String,
val shortcode: String,
val display_url: String,
val thumbnail_src: String,
val thumbnail_resources: ArrayList<InstagramThumbnailResourceResponse>,
val is_video: Boolean,
val accessibility_caption: String
)
data class InstagramThumbnailResourceResponse(
val src: String,
val config_width: Int,
val config_height: Int
)
}
Simply again, I want to just remove elements from the arraylist that match certain things what I don't want. For instance. the "is_video" value that comes from the json. I want to go through the arraylist and remove all elements that have "is_video" as true.
Thanks
If you asking how to filter the list then below is the demo.
You just need to use filter on your data which is an ArrayList. I've tried keeping the same structure for the models so that you can get a better understanding.
fun main() {
val first = InstagramNodeResponse(
title = "first",
is_video = true
)
val second = InstagramNodeResponse(
title = "second",
is_video = false
)
val list: ArrayList<InstagramEdgesResponse> = arrayListOf(
InstagramEdgesResponse(node = first),
InstagramEdgesResponse(node = second)
)
val itemsWithVideo = list.filter { it.node.is_video == true }
val itemsWithoutVideo = list.filter { it.node.is_video == false }
println(itemsWithVideo.map { it.node.title }) // [first]
println(itemsWithoutVideo.map { it.node.title }) // [second]
}
// Models
data class InstagramEdgesResponse(val node: InstagramNodeResponse)
data class InstagramNodeResponse(
val title: String,
val is_video: Boolean
)

How to combine two live data one after the other?

I have next use case: User comes to registration form, enters name, email and password and clicks on register button. After that system needs to check if email is taken or not and based on that show error message or create new user...
I am trying to do that using Room, ViewModel and LiveData. This is some project that on which I try to learn these components and I do not have remote api, I will store everything in local database
So I have these classes:
RegisterActivity
RegisterViewModel
User
UsersDAO
UsersRepository
UsersRegistrationService
So the idea that I have is that there will be listener attached to register button which will call RegisterViewModel::register() method.
class RegisterViewModel extends ViewModel {
//...
public void register() {
validationErrorMessage.setValue(null);
if(!validateInput())
return;
registrationService.performRegistration(name.get(), email.get(), password.get());
}
//...
}
So that is the basic idea, I also want for performRegistration to return to me newly created user.
The thing that bothers me the most is I do not know how to implement performRegistration function in the service
class UsersRegistrationService {
private UsersRepository usersRepo;
//...
public LiveData<RegistrationResponse<Parent>> performRegistration(String name, String email, String password) {
// 1. check if email exists using repository
// 2. if user exists return RegistrationResponse.error("Email is taken")
// 3. if user does not exists create new user and return RegistrationResponse(newUser)
}
}
As I understand, methods that are in UsersRepository should return LiveData because UsersDAO is returning LiveData
#Dao
abstract class UsersDAO {
#Query("SELECT * FROM users WHERE email = :email LIMIT 1")
abstract LiveData<User> getUserByEmail(String email);
}
class UsersRepository {
//...
public LiveData<User> findUserByEmail(String email) {
return this.usersDAO.getUserByEmail(email);
}
}
So my problem is how to implement performRegistration() function and how to pass value back to view model and then how to change activity from RegisterActivity to MainActivity...
You can use my helper method:
val profile = MutableLiveData<ProfileData>()
val user = MutableLiveData<CurrentUser>()
val title = profile.combineWith(user) { profile, user ->
"${profile.job} ${user.name}"
}
fun <T, K, R> LiveData<T>.combineWith(
liveData: LiveData<K>,
block: (T?, K?) -> R
): LiveData<R> {
val result = MediatorLiveData<R>()
result.addSource(this) {
result.value = block(this.value, liveData.value)
}
result.addSource(liveData) {
result.value = block(this.value, liveData.value)
}
return result
}
With the help of MediatorLiveData, you can combine results from multiple sources. Here an example of how would I combine two sources:
class CombinedLiveData<T, K, S>(source1: LiveData<T>, source2: LiveData<K>, private val combine: (data1: T?, data2: K?) -> S) : MediatorLiveData<S>() {
private var data1: T? = null
private var data2: K? = null
init {
super.addSource(source1) {
data1 = it
value = combine(data1, data2)
}
super.addSource(source2) {
data2 = it
value = combine(data1, data2)
}
}
override fun <S : Any?> addSource(source: LiveData<S>, onChanged: Observer<in S>) {
throw UnsupportedOperationException()
}
override fun <T : Any?> removeSource(toRemove: LiveData<T>) {
throw UnsupportedOperationException()
}
}
here is the gist for above, in case it is updated on the future:
https://gist.github.com/guness/0a96d80bc1fb969fa70a5448aa34c215
One approach is to use flows for this.
val profile = MutableLiveData<ProfileData>()
val user = MutableLiveData<CurrentUser>()
val titleFlow = profile.asFlow().combine(user.asFlow()){ profile, user ->
"${profile.job} ${user.name}"
}
And then your Fragment/Activity:
viewLifecycleOwner.lifecycleScope.launch {
viewModel.titleFlow.collectLatest { title ->
Log.d(">>", title)
}
}
One advantage to this approach is that titleFlow will only emit value when both live datas have emitted at least one value. This interactive diagram will help you understand this https://rxmarbles.com/#combineLatest
Alternative syntax:
val titleFlow = combine(profile.asFlow(), user.asFlow()){ profile, user ->
"${profile.job} ${user.name}"
}
Jose Alcérreca has probably the best answer for this:
fun blogpostBoilerplateExample(newUser: String): LiveData<UserDataResult> {
val liveData1 = userOnlineDataSource.getOnlineTime(newUser)
val liveData2 = userCheckinsDataSource.getCheckins(newUser)
val result = MediatorLiveData<UserDataResult>()
result.addSource(liveData1) { value ->
result.value = combineLatestData(liveData1, liveData2)
}
result.addSource(liveData2) { value ->
result.value = combineLatestData(liveData1, liveData2)
}
return result
}
without custom class
MediatorLiveData<Pair<Foo?, Bar?>>().apply {
addSource(fooLiveData) { value = it to value?.second }
addSource(barLiveData) { value = value?.first to it }
}.observe(this) { pair ->
// TODO
}
I did an approach based on #guness answer. I found that being limited to two LiveDatas was not good. What if we want to use 3? We need to create different classes for every case. So, I created a class that handles an unlimited amount of LiveDatas.
/**
* CombinedLiveData is a helper class to combine results from multiple LiveData sources.
* #param liveDatas Variable number of LiveData arguments.
* #param combine Function reference that will be used to combine all LiveData data results.
* #param R The type of data returned after combining all LiveData data.
* Usage:
* CombinedLiveData<SomeType>(
* getLiveData1(),
* getLiveData2(),
* ... ,
* getLiveDataN()
* ) { datas: List<Any?> ->
* // Use datas[0], datas[1], ..., datas[N] to return a SomeType value
* }
*/
class CombinedLiveData<R>(vararg liveDatas: LiveData<*>,
private val combine: (datas: List<Any?>) -> R) : MediatorLiveData<R>() {
private val datas: MutableList<Any?> = MutableList(liveDatas.size) { null }
init {
for(i in liveDatas.indices){
super.addSource(liveDatas[i]) {
datas[i] = it
value = combine(datas)
}
}
}
}
You can define a method that would combine multiple LiveDatas using a MediatorLiveData, then expose this combined result as a tuple.
public class CombinedLiveData2<A, B> extends MediatorLiveData<Pair<A, B>> {
private A a;
private B b;
public CombinedLiveData2(LiveData<A> ld1, LiveData<B> ld2) {
setValue(Pair.create(a, b));
addSource(ld1, (a) -> {
if(a != null) {
this.a = a;
}
setValue(Pair.create(a, b));
});
addSource(ld2, (b) -> {
if(b != null) {
this.b = b;
}
setValue(Pair.create(a, b));
});
}
}
If you need more values, then you can create a CombinedLiveData3<A,B,C> and expose a Triple<A,B,C> instead of the Pair, etc. Just like in https://stackoverflow.com/a/54292960/2413303 .
EDIT: hey look, I even made a library for you that does that from 2 arity up to 16: https://github.com/Zhuinden/livedata-combinetuple-kt
Many of these answers work, but also it is assumed the LiveData generic types are not-nullable.
But what if one or more of the given input types are nullable types (given the default Kotlin upper bound for generics is Any?, which is nullable)?
The result would be even though the LiveData emitter would emit a value (null), the MediatorLiveData will ignore it, thinking it's his own child live data value not being set.
This solution, instead, takes care of it by forcing the upper bound of the types passed to the mediator to be not null. Lazy but needed.
Also, this implementation avoids same-value after the combiner function has been called, which might or might not be what you need, so feel free to remove the equality check there.
fun <T1 : Any, T2 : Any, R> combineLatest(
liveData1: LiveData<T1>,
liveData2: LiveData<T2>,
combiner: (T1, T2) -> R,
): LiveData<R> = MediatorLiveData<R>().apply {
var first: T1? = null
var second: T2? = null
fun updateValueIfNeeded() {
value = combiner(
first ?: return,
second ?: return,
)?.takeIf { it != value } ?: return
}
addSource(liveData1) {
first = it
updateValueIfNeeded()
}
addSource(liveData2) {
second = it
updateValueIfNeeded()
}
}
LiveData liveData1 = ...;
LiveData liveData2 = ...;
MediatorLiveData liveDataMerger = new MediatorLiveData<>();
liveDataMerger.addSource(liveData1, value -> liveDataMerger.setValue(value));
liveDataMerger.addSource(liveData2, value -> liveDataMerger.setValue(value));
if you want both value not null
fun <T, V, R> LiveData<T>.combineWithNotNull(
liveData: LiveData<V>,
block: (T, V) -> R
): LiveData<R> {
val result = MediatorLiveData<R>()
result.addSource(this) {
this.value?.let { first ->
liveData.value?.let { second ->
result.value = block(first, second)
}
}
}
result.addSource(liveData) {
this.value?.let { first ->
liveData.value?.let { second ->
result.value = block(first, second)
}
}
}
return result
}
If you want to create a field and setup at construction time (use also):
val liveData1 = MutableLiveData(false)
val liveData2 = MutableLiveData(false)
// Return true if liveData1 && liveData2 are true
val liveDataCombined = MediatorLiveData<Boolean>().also {
// Initial value
it.value = false
// Observing changes
it.addSource(liveData1) { newValue ->
it.value = newValue && liveData2.value!!
}
it.addSource(selectedAddOn) { newValue ->
it.value = liveData1.value!! && newValue
}
}
Solved with LiveData extensions
fun <T, R> LiveData<T>.map(action: (t: T) -> R): LiveData<R> =
Transformations.map(this, action)
fun <T1, T2, R> LiveData<T1>.combine(
liveData: LiveData<T2>,
action: (t1: T1?, t2: T2?) -> R
): LiveData<R> =
MediatorLiveData<Pair<T1?, T2?>>().also { med ->
med.addSource(this) { med.value = it to med.value?.second }
med.addSource(liveData) { med.value = med.value?.first to it }
}.map { action(it.first, it.second) }
Java version, if anyone else is stuck working on some old project
var fullNameLiveData = LiveDataCombiner.combine(
nameLiveData,
surnameLiveData,
(name, surname) -> name + surname
)
public class LiveDataCombiner<First, Second, Combined> {
private First first;
private Second second;
private final MediatorLiveData<Combined> combined = new MediatorLiveData<>();
private final BiFunction<First, Second, Combined> combine;
public LiveData<Combined> getCombined() {
return combined;
}
public static <First, Second, Combined>LiveDataCombiner<First, Second, Combined> combine(
LiveData<First> firstData,
LiveData<Second> secondData,
BiFunction<First, Second, Combined> combine
) {
return new LiveDataCombiner<>(firstData, secondData, combine);
}
private LiveDataCombiner(
LiveData<First> firstData,
LiveData<Second> secondData,
BiFunction<First, Second, Combined> combine
) {
this.combine = combine;
addSource(firstData, value -> first = value);
addSource(secondData, value -> second = value);
}
private <T> void addSource(LiveData<T> source, Consumer<T> setValue) {
combined.addSource(source, second -> {
setValue.accept(second);
emit(combine());
});
}
private Combined combine() {
return combine.apply(first, second);
}
private void emit(Combined value) {
if (combined.getValue() != value)
combined.setValue(value);
}
}

Categories

Resources