I want to load data from Firestore, and combine it with other data using Flow combine()
ViewModel:
private val userCurrentProject = MutableStateFlow("")
val projects = repository
.listenToProject() //listening via Firestore snapshot listener, no problem here
.combine(userCurrentProject) { projects, currentProjectName ->
// combine works and called normally
projects.map { project ->
project.apply {
isUserCurrentProject = name == currentProjectName
}
}
}
fun setCurrentProject(projectName: String) = viewModelScope.launch {
userCurrentProject.emit(projectName)
}
Composables:
fun ProjectListScreen(navController: NavHostController, viewModel: ProjectsViewModel) {
val projects by viewModel.projects.collectAsState(initial = emptyList())
// This is where the problem started
// Lazy column not updated when projects flow is emitting new value
// Even Timber log does not called
Timber.d("Projects : $projects")
LazyColumn {
items(projects) { project ->
ProjectItem(project = project) {
currentlySelectedProject = project
scope.launch { bottomSheetState.show() }
}
}
}
The flow is working normally, but the state never got updated, I don't know why. Maybe this is a problem with collectAsState()?
But the state is updated when I navigate to next screen (add new project screen), then press back (popBackStack)
NB: using asLiveData() with observeAsState() does not work either.
I've finally found the answer
The culprit is that a State of custom object/class behaves differently than a state of primitives (String, Int, etc.)
For a State of object, you need to use copy()
So I just changed this part of ViewModel
val projects = repository
.listenProject()
.combine(userCurrentProject) { projects, currentProjectName ->
projects.map { project ->
// use copy instead of apply
val isCurrentProject = project.name == currentProjectName
project.copy(isUserCurrentProject = isCurrentProject)
}
}
Related
I have a StateFlow from which my List composable collects any changes as a State.
private val _people = MutableStateFlow(personDataList())
val people = _people.asStateFlow()
And inside my viewModel, I perform modifications on _people and I verify that people as a read-only StateFlow is also getting updated. I also have to make a copy of the original _people as an ordinary kotlin map to use for some verifications use-cases.
val copyAsMap : StateFlow<MutableMap<Int, Person>> = people.map {
it.associateBy( { it.id }, { it } )
.toMutableMap()
}.stateIn(viewModelScope, SharingStarted.Eagerly, mutableMapOf())
however, with my attempt above, it (the copyAsMap) doesn't get updated when I try to modify the list (e.g delete) an item from the _people StateFlow
Any ideas..? Thanks!
Edit:
Nothing is collecting from the copyAsMap, I just display the values everytime an object is removed from _person state flow
delete function (triggered by an action somewhere)
private fun delete(personModel: Person) {
_person.update { list ->
list.toMutableStateList().apply {
removeIf { it.id == personModel.id }
}
}
copyAsMap.values.forEach {
Log.e("MapCopy", "$it")
}
}
So based on your comment how you delete the item, that's the problem:
_people.update { list ->
list.removeIf { it.id == person.id }
list
}
You get an instance of MutableList here, do the modification and you "update" the flow with the same instance. And, as StateFlow documentation says:
Values in state flow are conflated using Any.equals comparison in a similar way to distinctUntilChanged operator. It is used to conflate incoming updates to value in MutableStateFlow and to suppress emission of the values to collectors when new value is equal to the previously emitted one.
Which means that your updated list is actually never emitted, because it is equal to the previous value.
You have to do something like this:
_people.update { list ->
list.toMutableList().apply { removeIf { ... } }
}
Also, you should define your state as val _people: MutableStateFlow<List<T>> = .... This would prevent some mistakes you can make.
I am trying to show a list of Orders in a list using LazyColumn. Here is the code:
#Composable
private fun MyOrders(
orders: List<Order>?,
onClick: (String, OrderStatus) -> Unit
) {
orders?.let {
LazyColumn {
items(
items = it,
key = { it.id }
) {
OrderDetails(it, onClick)
}
}
}
}
#Composable
private fun OrderDetails(
order: Order,
onClick: (String, OrderStatus) -> Unit
) {
println("Composing Order Item")
// Item Code Here
}
Here is the way, I call the composable:
orderVm.fetchOrders()
val state by orderVm.state.collectAsState(OrderState.Empty)
if (state.orders.isNotEmpty()) {
MyOrders(state.orders) {
// Handle status change click listener
}
}
I fetch all my orders and show in the LazyColumn. However, when a single order is updated, the entire LazyColumn gets rrecomposed. Here is my ViewModel looks like:
class OrderViewModel(
fetchrderUseCase: FetechOrdersUseCase,
updateStatusUseCase: UpdateorderUseCase
) {
val state = MutableStateFlow(OrderState.Empty)
fun fetchOrders() {
fetchrderUseCase().collect {
state.value = state.value.copy(orders = it.data)
}
}
fun updateStatus(newStatus: OrderStatus) {
updateStatusUseCase(newStatus).collect {
val oldOrders = status.value.orders
status.value = status.value.copy(orders = finalizeOrders(oldOrders))
}
}
}
NOTE: The finalizeOrders() does some list manipulation based on orderId to update one order with the updated one.
This is how my state looks like:
data class OrderState(
val orders: List<Order> = listOf(),
val isLoading: Boolean = false,
val error: String = ""
) {
companion object {
val Empty = FetchOrdersState()
}
}
If I have 10 orders in my DB and I update one's status (let's say 5th item), then OrderDetails gets called for 20 times. Not sure why. Caan I optimize it to make sure only the 5th indexed item will be recomposed and the OrderDetals gets called only with the new order.
Is the Orderclasss stable? If not it could be the reason why all the items get recomposed:
Compose skips the recomposition of a composable if all the inputs are stable and haven't changed. The comparison uses the equals method
This section in the compose's doc explains what are stable types and how to skip recomposition.
Note: If you scroll a lazy list, all invisible items will be destroyed. That means if you scroll back they will be recreated not recomposed (you can't skip recreation even if the input is stable).
I'm using Axe DevTools and I'm trying to figure out how to tag multiple scans with the same build information. Right now I have my tests running like this:
class MyTestCase : XCTestCase {
func myTest() {
Attest.that(view: view)
.isAccessible({ result in })
.andPushResult(withTags: [myBuild])
}
}
How can I add the myBuild tag globally to all tests that I run?
I would build my own class that utilizes the Axe DevTools (Attest) APIs. Then have my test cases interact with my own class instead of interacting with Attest itself!
class AccessibilityTestUtils {
static let buildTag:String = Bundle.main.object(
forInfoDictionaryKey: "CFBundleShortVersionString"
) as! String
init(build: String) {
self.buildTag = build
}
static func runAccessibilityTestOn(aView : View) {
Attest.that(view: aView).isAccessible({ result in })
.andPushResult(withTags: [buildTag])
}
}
Example Usage
class YourTestClass {
func yourTestCase() {
AccessibilityTestUtils.runAccessibilityTestOn(aView)
}
}
Note: This approach also protects you from future changes to the Attest library by making it so that you only have to change one line of code in the event of non backwards compatible changes.
I am trying to migrate some code using a Repository pattern from Vapor 3 to Vapor 4. I have gone through the documentation of this specific pattern from the Vapor 4 documentation, and I think I understand it for the most part.
The one thing I am not getting, however, is the way that the repository factory gets set within the Application extension. The example from the documentation shows this:
extension Application {
private struct UserRepositoryKey: StorageKey {
typealias Value = UserRepositoryFactory
}
var users: UserRepositoryFactory {
get {
self.storage[UserRepositoryKey.self] ?? .init()
}
set {
self.storage[UserRepositoryKey.self] = newValue
}
}
}
If I am reading the getter method correctly (and I might not be - I'm far from a Swift expert), a new instance of the UserRepositoryFactory structure will be created and returned when app.users is referenced. At that time, however, it does not appear that the contents of self.storage[UserRepositoryKey.self] is changed in any way. So if I happened to access app.users two times in a row, I would get 2 different instances returned to me and self.storage[UserRepositoryKey.self] would remain set to nil.
Following through the rest of the sample code in the document, it appears to define the make function that will be used by the factory when configuring the app as so:
app.users.use { req in
DatabaseUserRepository(database: req.db)
}
Here it seems like app.users.use would get a new factory instance and call its use function to set the appropriate make method for that instance.
Later, when I go to handle a request, I use the request.users method that was defined by this Request extension:
extension Request {
var users: UserRepository {
self.application.users.make!(self)
}
}
Here it seems like self.application.users.make would be invoked on a different repository factory instance that is referenced by self.application.users. It would therefore not apply the factory's make method that was set earlier when configuring the application.
So what am I missing here?
It looks like the docs are slightly out of date for that. You can have a look at how views or client is done, but somewhere you need to call initialize() to set the repository. Here's what my working repository looks like:
import Vapor
extension Application {
struct Repositories {
struct Provider {
let run: (Application) -> ()
public init(_ run: #escaping (Application) -> ()) {
self.run = run
}
}
final class Storage {
var makeRepository: ((Application) -> APIRepository)?
init() { }
}
struct Key: StorageKey {
typealias Value = Storage
}
let application: Application
var repository: APIRepository {
guard let makeRepository = self.storage.makeRepository else {
fatalError("No repository configured. Configure with app.repositories.use(...)")
}
return makeRepository(self.application)
}
func use(_ provider: Provider) {
provider.run(self.application)
}
func use(_ makeRepository: #escaping (Application) -> APIRepository) {
self.storage.makeRepository = makeRepository
}
func initialize() {
self.application.storage[Key.self] = .init()
}
private var storage: Storage {
if self.application.storage[Key.self] == nil {
self.initialize()
}
return self.application.storage[Key.self]!
}
}
var repositories: Repositories {
.init(application: self)
}
}
That autoinitializes itself the first time it's used. Note that APIRepository is the protocol used for my repostiory. FluentRepository is the Fluent implementation of that protocol. Then like you I have an extension on Request to use it in request handlers:
extension Request {
var repository: APIRepository {
self.application.repositories.repository.for(self)
}
}
Finally, you need to configure it to use the right repository. So in my configure.swift I have:
app.repositories.use { application in
FluentRepository(database: application.db)
}
and in tests I can switch it for the in-memory repository that doesn't touch the DB:
application.repositories.use { _ in
return inMemoryRepository
}
I have managed to get the example from the docs working as-is.
Tracing through the execution with the debugger, there is the predictable call to get, as you say, and this returns the instance from .init() as the failover from not having a previously stored value. Included in the example you refer to is:
struct UserRepositoryFactory {
var make: ((Request) -> UserRepository)?
mutating func use(_ make: #escaping ((Request) -> UserRepository)) {
self.make = make
}
}
This use function is executed next, which is mutating and updates the variable make. I believe it is this change to make that then triggers a call to set. It certainly happens immediately after use and before execution moves on in configure.swift. So, by the time the server formally starts and you actually use the Repository in a route, there is a stored instance that is reused as required.
I just updated my Android Studio to version 3.2 and followed instructions to use androidx.
I've been using a Youtube fragment inside a Fragment activity and everything worked perfectly but, after the update, these 3 simple lines now give me the error "Cannot resolve method 'add(...)'":
YouTubePlayerSupportFragment youTubePlayerFragment = YouTubePlayerSupportFragment.newInstance();
FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
transaction.add(R.id.youtube_fragment, youTubePlayerFragment).commit();
...and when i try to use "replace" instead of "add" it says: "Wrong 2nd argument type. Found: 'com.google.android.youtube.player.YouTubePlayerSupportFragment', required: 'androidx.fragment.app.Fragment'"
...which makes me think that the problem has to do with the new AndroidX feature.
The problem is that the add method wants the second parameter of type:
androidx.fragment.app.Fragment
...but the YouTubePlayerSupportFragment returns a:
android.support.v4.app.Fragment
Does anyone know how to solve this problem?
Is there a way to cast the "android.support.v4.app.Fragment" into the "androidx.fragment.app.Fragment"?
Just use transaction.replace. Ignore the error, it'll work. Google hasn't refactored youtube api library to androidx yet.
Just copy the original java file (com.google.android.youtube.player.YouTubePlayerFragment) to your project to the same package but different class name etc. com.google.android.youtube.player.YouTubePlayerFragmentX, and update the extends class from android.app.Fragment to androidx.fragment.app.Fragment.
The implementation is the same:
YouTubePlayerFragmentX youTubePlayerFragment = YouTubePlayerFragmentX.newInstance();
FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
transaction.add(R.id.youtube_fragment, youTubePlayerFragment).commit();
Tested... it's working.
I've fixed it by following the #Hosszful answer,
I made it easy by just using this file, https://gist.github.com/medyo/f226b967213c3b8ec6f6bebb5338a492
Replace .add
transaction.add(R.id.youtube_fragment, youTubePlayerFragment).commit();
with this .replace
transaction.replace(R.id.youtube_fragment, youTubePlayerFragment).commit();
and copy this class to your project folder (it may need to create the following folders)
java -> com -> google -> android -> youtube -> player -> (here name of)
YouTubePlayerSupportFragmentX.java
then in code replace
YouTubePlayerSupportFragment
to
YouTubePlayerSupportFragmentX.
Many thanks to both #Hosszuful and #Mehdi. I have followed your advice and it worked very nicely.
A few weeks after I asked this question I "translated" my app to Kotlin and, therefore, I tried to translate your answer as well.
This is what I ended up with and it's working for me.
package com.google.android.youtube.player //<--- IMPORTANT!!!!
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.google.android.youtube.player.internal.ab
import java.util.*
class YouTubePlayerSupportFragmentX : Fragment(), YouTubePlayer.Provider {
private val a = ViewBundle()
private var b: Bundle? = null
private var c: YouTubePlayerView? = null
private var d: String? = null
private var e: YouTubePlayer.OnInitializedListener? = null
override fun initialize(var1: String, var2: YouTubePlayer.OnInitializedListener) {
d = ab.a(var1, "Developer key cannot be null or empty")
e = var2
a()
}
private fun a() {
if (c != null && e != null) {
c?.a(this.activity, this, d, e, b)
b = null
e = null
}
}
override fun onCreate(var1: Bundle?) {
super.onCreate(var1)
b = var1?.getBundle("YouTubePlayerSupportFragment.KEY_PLAYER_VIEW_STATE")
}
override fun onCreateView(var1: LayoutInflater, var2: ViewGroup?, var3: Bundle?): android.view.View? {
c = YouTubePlayerView(Objects.requireNonNull(this.activity), null, 0, a)
a()
return c
}
override fun onStart() {
super.onStart()
c?.a()
}
override fun onResume() {
super.onResume()
c?.b()
}
override fun onPause() {
c?.c()
super.onPause()
}
override fun onSaveInstanceState(var1: Bundle) {
super.onSaveInstanceState(var1)
(if (c != null) c?.e() else b)?.let { var2 ->
var1.putBundle("YouTubePlayerSupportFragment.KEY_PLAYER_VIEW_STATE", var2)
}
}
override fun onStop() {
c?.d()
super.onStop()
}
override fun onDestroyView() {
this.activity?.let { c?.c(it.isFinishing) }
c = null
super.onDestroyView()
}
override fun onDestroy() {
if (c != null) {
val var1 = this.activity
c?.b(var1 == null || var1.isFinishing)
}
super.onDestroy()
}
private inner class ViewBundle : YouTubePlayerView.b {
override fun a(var1: YouTubePlayerView, var2: String, var3: YouTubePlayer.OnInitializedListener) {
e?.let { initialize(var2, it) }
}
override fun a(var1: YouTubePlayerView) {}
}
companion object {
fun newInstance(): YouTubePlayerSupportFragmentX {
return YouTubePlayerSupportFragmentX()
}
}
}
There may be better ways to write it down and any help on that regard would be mostly appreciated but, if anyone else was looking for the Kotlin version of this problem's solution, this code should do.
PS: I'm gonna leave #Mehdi's answer as the accepted one because he's also sharing credits with #Hosszuful and because my answer is just the Kotlin version of what they suggest.
I got it working by following code chunk.
Object obj =
getSupportFragmentManager().findFragmentById(R.id.youtube_player_fragment);
if (obj instanceof YouTubePlayerSupportFragment)
youTubePlayerFragment = (YouTubePlayerSupportFragment) obj;
During debugging I found that the fragmentmanager was coming to be instance of YouTubePlayerSupportFragment only. But compiler was not able to cast it when I would write
(YouTubePlayerSupportFragment)
getSupportFragmentManager().findFragmentById(R.id.youtube_player_fragment);
The above code chunk (instanceof ) worked fine.
Suggested solutions did not work, till I tried the comment from Bek: Pierfrancesco Soffritti's android-youtube-player that is maintained and works without a hitch.