How I can update my adapter for RecyclerView after change my LiveData? - mvvm

I created a fragment that in the onActivityCreated method fetches Firebase data by limiting the query to a calendar date. Then I place Observers on my LiveData that are inside my ViewModel and that will deliver the list to my Adapter.
If I add, remove or update items in the same list, the changes are sent to firebase and the adapter reflects them on the screen. It works ok.
But, I am trying to develop a filter button, which will basically change the deadline date for the Firebase query. When I select a particular filter, the viewModel needs to retrieve the data from Firebase limited to the filter date. This generates a new list, having a different size from the previous one.
However, when the query occurs, the Adapter's getItemCount() method stores the size of the last list. This fact confuses the Adapter and the functions notifyItemInserted and notifyItemRemoved end up making confusing animations on the screen after changing the filter. I dont know whats is wrong.
How can I correctly observes LiveData and tell the adapter? Am I making a mistake in the MVVM architecture or forgetting some function?
My Fragment:
class HistoryFragment : Fragment(), OnItemMenuRecyclerViewClickListener {
private lateinit var mSecurityPreferences: SecurityPreferences
private lateinit var viewModel: BalancesViewModel
private lateinit var adapter: BalancesAdapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
setHasOptionsMenu(true)
viewModel = ViewModelProvider(this).get(BalancesViewModel::class.java)
adapter = BalancesAdapter(requireContext())
mSecurityPreferences = SecurityPreferences(requireContext())
return inflater.inflate(R.layout.fragment_history, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
setupFilter()
//Setup adapter
adapter.listenerMenu = this
recycler_view_history.adapter = adapter
//Fetch data based in filter by date
viewModel.fetchBalances(mSecurityPreferences.getStoredLong(FILTER_DATE))
// Put logic to listen RealTimeUpdates
viewModel.getRealTimeUpdates(mSecurityPreferences.getStoredLong(FILTER_DATE))
viewModel.balances.observe(viewLifecycleOwner, Observer {
adapter.setBalances(it)
})
viewModel.balance.observe(viewLifecycleOwner, Observer {
adapter.addBalance(it)
})
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.history_menu_filter, menu)
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.item_menu_filter_this_month -> {
updateFilter(THIS_MONTH)
}
R.id.item_menu_filter_two_months -> {
updateFilter(TWO_MONTHS)
}
R.id.item_menu_filter_last_six_months -> {
updateFilter(LAST_SIX_MONTHS)
}
R.id.item_menu_filter_all -> {
updateFilter(ALL_MONTHS)
}
}
return super.onOptionsItemSelected(item)
}
private fun setupFilter() {
var filterOption = mSecurityPreferences.getStoredLong(FILTER_DATE)
if (filterOption == 0L){
filterOption = HandleDate.getLongToFilter(LAST_SIX_MONTHS)
mSecurityPreferences.storeLong(FILTER_DATE, filterOption)
}
}
private fun updateFilter(filterOption: Int){
val newFilterOption = HandleDate.getLongToFilter(filterOption)
mSecurityPreferences.storeLong(FILTER_DATE, newFilterOption)
updateUI()
}
private fun updateUI(){
viewModel.fetchBalances(mSecurityPreferences.getStoredLong(FILTER_DATE))
viewModel.getRealTimeUpdates(mSecurityPreferences.getStoredLong(FILTER_DATE))
}
}
My ViewModel:
class BalancesViewModel : ViewModel() {
private val userReference = FirebaseAuth.getInstance().currentUser!!.uid
private val dbUserReference = FirebaseDatabase.getInstance().getReference(userReference)
private val _balances = MutableLiveData<List<Balance>>()
val balances: LiveData<List<Balance>>
get() = _balances
private val _balance = MutableLiveData<Balance>()
val balance: LiveData<Balance>
get() = _balance
private val _result = MutableLiveData<Exception?>()
val result: LiveData<Exception?>
get() = _result
fun addBalance(balance: Balance) {
balance.id = dbUserReference.push().key
dbUserReference.child(NODE_BALANCES).child(balance.id!!).setValue(balance)
.addOnCompleteListener {
if (it.isSuccessful) {
_result.value = null
} else {
_result.value = it.exception
}
}
}
private val childEventListener = object : ChildEventListener {
override fun onCancelled(error: DatabaseError) {
}
override fun onChildMoved(snapshot: DataSnapshot, p1: String?) {
}
override fun onChildChanged(snapshot: DataSnapshot, p1: String?) {
val balance = snapshot.getValue(Balance::class.java)
balance?.id = snapshot.key
_balance.value = balance
}
override fun onChildAdded(snapshot: DataSnapshot, p1: String?) {
val balance = snapshot.getValue(Balance::class.java)
balance?.id = snapshot.key
_balance.value = balance
}
override fun onChildRemoved(snapshot: DataSnapshot) {
val balance = snapshot.getValue(Balance::class.java)
balance?.id = snapshot.key
balance?.isDeleted = true
_balance.value = balance
}
}
fun getRealTimeUpdates(longLimitDate: Long) {
dbUserReference.child(NODE_BALANCES).orderByChild(COLUMN_DATE_MILLI)
.startAt(longLimitDate.toDouble()).addChildEventListener(childEventListener)
}
fun fetchBalances(longLimitDate: Long) {
dbUserReference.child(NODE_BALANCES).orderByChild(COLUMN_DATE_MILLI)
.startAt(longLimitDate.toDouble())
.addListenerForSingleValueEvent(object : ValueEventListener {
override fun onCancelled(error: DatabaseError) {}
override fun onDataChange(snapshot: DataSnapshot) {
if (snapshot.exists()) {
val listBalances = mutableListOf<Balance>()
for (balanceSnapshot in (snapshot.children)) {
val balance = balanceSnapshot.getValue(Balance::class.java)
balance?.id = balanceSnapshot.key
balance?.let { listBalances.add(it) }
}
listBalances.sortByDescending { it.dateMilli }
_balances.value = listBalances
}
}
})
}
fun updateBalance(balance: Balance) {
dbUserReference.child(NODE_BALANCES).child(balance.id!!).setValue(balance)
.addOnCompleteListener {
if (it.isSuccessful) {
_result.value = null
} else {
_result.value = it.exception
}
}
}
fun deleteBalance(balance: Balance) {
dbUserReference.child(NODE_BALANCES).child(balance.id!!).setValue(null)
.addOnCompleteListener {
if (it.isSuccessful) {
_result.value = null
} else {
_result.value = it.exception
}
}
}
My Adapter:
class BalancesAdapter(private val context: Context) :
RecyclerView.Adapter<BalancesAdapter.BalanceViewModel>() {
private var balances = mutableListOf<Balance>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
BalanceViewModel(
LayoutInflater.from(parent.context)
.inflate(R.layout.item_recyclerview_balance, parent, false)
)
override fun getItemCount() = balances.size
override fun onBindViewHolder(holder: BalanceViewModel, position: Int) {
holder.view.text_view_value_balance_item.text = balances[position].value
holder.view.text_view_date_item.text = balances[position].date
}
fun setBalances(balances: List<Balance>) {
this.balances = balances as MutableList<Balance>
notifyDataSetChanged()
}
fun addBalance(balance: Balance) {
val index = balances.indexOf(balance)
if (!balances.contains(balance)) {
balances.add(balance)
notifyItemInserted(index)
} else {
if (balance.isDeleted) {
balances.removeAt(index)
notifyItemRemoved(index)
} else {
balances[index] = balance
}
}
notifyItemRangeChanged(index, itemCount)
}
class BalanceViewModel(val view: View) : RecyclerView.ViewHolder(view)
}
Tnks for your attention.

Okay, it's been 4 days since I asked this question and after feeling a little frustrated with the project I come back here on StackOverFlow to post my own answer.
The problematic issue within the code I showed is in my Adapter's addBalance method.
When I created the Balance data model, I set the isDeleted attribute to identify that it was deleted. Upon entering Firebase it receives a NULL value and therefore it ceases to exist.
Then, as I have two listeners (one defined in the addListenerForSingleValueEvent method and the other defined in the addChildEventListener method), one ends up triggering the other when there is a change in the Firebase data, but I don't want to go into detail on that issue. The fact is that I checked that my addBalance method was being called after I deleted an object, causing that object to be inserted back into the Adapter's data list, even before the removal operation ended in Firebase.
So I changed the logic of my method to make sure that my object was deleted and only included it in my Adapter list after checking the isDeleted attribute.
fun dealWithBalance(balance: Balance){
val index = balances.indexOf(balance)
if(balance.isDeleted && balances.contains(balance)){
balances.removeAt(index)
notifyItemRemoved(index)
} else if(!balance.isDeleted && !balances.contains(balance)){
balances.add(balance)
} else if(index >= 0){
balances[index] = balance
notifyItemChanged(index)
}
}
I renamed addBalance to dealWithBalance...

Related

Is it necessary to proxy data in viewModel with MutableStateFlow?

Recently in Android viewModel was useful because of liveData, but currently is it good to verbose your code and proxy all calls from Composable? Or it is fine to call subclass methods directly and left viewModel for merging modules like repositories, dependency injections and some temporal UI states? E.g. is this fine:
#Composable
fun startView(viewModel: MyViewModel) {
Column {
viewModel.space.foos.forEach {
doSomethingWithFoo(it)
}
}
}
#Composable
fun doSomethingWithFoo(foo: Foo) {
IconButton(onClick = { foo.doTheThing(42) }) {
Icon(imageVector = foo.icon, contentDescription = null)
}
}
class MyViewModel() : ViewModel() {
val space = Space()
}
class Space() {
val foos = listOf(Foo(), Foo())
}
class Foo() {
val bar = mutableStateOf(0)
val icon = Icons.Default.Abc
fun doTheThing(i: Int) {
bar.value = i
}
}
or it's better to write a proxy method in viewModel instead of direct call foo.doTheThing(42)?

Flutter, iOS native | how to sink on multiple event streams per one FlutterEventChannel?

I cannot make the iOS native to sink events on two event streams per one channel. However, the Android native is working as expected, i.e. one channel can handle multiple event streams. On iOS native, it seams that only one event stream is possible per event channel? Anyone knows is this intended functionality or a bug in my code?
On Dart side I create one EventChannel and I expect to get stream events from two different event streams.
Here is my dart code:
static const EventChannel _xxxEventChannel = EventChannel('xxx_event_channel');
...
_xxxEventChannel.receiveBroadcastStream("aaa_stream_of_events").listen((event) {...});
...
_xxxEventChannel.receiveBroadcastStream("bbb_stream_of_events").listen((event) {...});
...
At some point on Dart code I start listening both "aaa_stream_of_events" and "bbb_stream_of_events". I can see on native side the onListen function is called called correctly and FlutterEventSink streams are registered. But when ever I sinkAaaStreamEvents("someData") also the "bbb_stream_of_events" listener is called on Dart side.
Here is roughly my iOS native code:
public class XxxNativePlugin: NSObject, FlutterPlugin, FlutterStreamHandler {
private var sinkAaaStreamEvents: FlutterEventSink?
private var sinkBbbStreamEvents: FlutterEventSink?
public static func register(with registrar: FlutterPluginRegistrar) {
let instance = XxxNativePlugin()
let xxxEventChannel = FlutterEventChannel(name: "xxx_event_channel", binaryMessenger: registrar.messenger())
xxxEventChannel.setStreamHandler(instance)
}
func onListen(withArguments arguments: Any?, eventSink events: #escaping FlutterEventSink) -> FlutterError? {
if let argument = arguments as? String {
if (argument == "aaa_stream_of_events") {
sinkAaaStreamEvents = events
} else if (argument == "bbb_stream_of_events") {
sinkBbbStreamEvents = events
} else {
// Unknown stream listener registered
}
}
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
if let argument = arguments as? String {
if (argument == "aaa_stream_of_events") {
sinkAaaStreamEvents = nil
} else if (argument == "bbb_stream_of_events") {
sinkBbbStreamEvents = nil
} else {
// Unknown stream listener unregistered
}
}
return nil
}
}
For comparison, on Android native everything is working as expected. i.e. one channel can handle multiple event streams:
class XxxNativePlugin : FlutterPlugin, EventChannel.StreamHandler {
private var xxxEventChannel: EventChannel? = null
private var sinkAaaStreamEvents: EventChannel.EventSink? = null
private var sinkBbbStreamEvents: EventChannel.EventSink? = null
override fun onAttachedToEngine(#NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
xxxEventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "xxx_event_channel")
xxxEventChannel?.setStreamHandler(this)
}
override fun onDetachedFromEngine(#NonNull binding: FlutterPlugin.FlutterPluginBinding) {
xxxEventChannel?.setStreamHandler(null)
xxxEventChannel = null
}
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
when (val arg = arguments as String) {
"aaa_stream_of_events" -> {
sinkAaaStreamEvents = events
}
"bbb_stream_of_events" -> {
sinkBbbStreamEvents = events
}
else {
// Unknown stream listener registered
}
}
override fun onCancel(arguments: Any?) {
Log.d(TAG, "onCancel arguments " + arguments?.toString())
when (val arg = arguments as String) {
"aaa_stream_of_events" -> {
sinkAaaStreamEvents = null
}
"bbb_stream_of_events" -> {
sinkBbbStreamEvents = null
}
else -> {
// Unknown stream listener unregistered
}
}
}
}

How to recursively iterate over Swift Syntax with SwiftSyntax library?

I would like to iterate in my code over the Swift AST like this, finding the struct keyword.
private func recursion(node: Syntax) -> String {
for child in node.children {
if let tokenKind = (child as? TokenSyntax)?.tokenKind, tokenKind == .structKeyword {
// if type(of: child) == StructDeclSyntax.self {
print ("yeah")
}
recursion(node: child)
}
}
let input = """
public struct cmd_deleteEdge<E: VEdge> : EdgeCommand {
public var keyEquivalent = KeyEquivalent.none
public let title = "Delete Edge"
public let id = "deleteEdge"
public let toolTip = "Delete selected Edge"
public let icon = Icon.delete
//receivers
public let edge: E
public init(edge: E) {
self.edge = edge
}
public func execute() throws -> ActionResult {
edge.deleteYourself()
return .success("deleted edge")
}
}
"""
public func convert(structText: String) throws -> String {
let sourceFile = try SyntaxParser.parse(source: structText)
let result = recursion(node: Syntax(sourceFile))
return result
}
try convert(structText: input)
It just simply doesn't work, I never reach the "Yeah" (which means I cannot do anything useful during the recursion).
I find this library very confusing. Would anyone have a good UML diagram explaining how it really works?
Before you tell me, yes I know I could use a Visitor, but I want to understand how it works by myself.
You can use SyntaxProtocol for iterating all items in AST and then use its _syntaxNode public property to make a target syntax, e.g.:
import SwiftSyntax
import SwiftSyntaxParser
func recursion(node: SyntaxProtocol) {
if let decl = StructDeclSyntax(node._syntaxNode) {
print(decl.identifier)
}
node.children.forEach { recursion(node: $0) }
}
let code = """
struct A {}
class Some {
struct B {}
}
func foo() {
struct C {}
}
"""
let sourceFile = try SyntaxParser.parse(source: code)
recursion(node: sourceFile)
Outputs:
A
B
C
NOTE: It is not recommended to retrieve _syntaxNode property directly and you can use Syntax(fromProtocol: node) instead.
SyntaxVisitor
But the best approach is using Visitor pattern with SyntaxVisitor class to avoid recursion issues for large and complex files:
class Visitor: SyntaxVisitor {
var structs = [StructDeclSyntax]()
init(source: String) throws {
super.init()
let sourceFile = try SyntaxParser.parse(source: source)
walk(sourceFile)
}
// MARK: - SyntaxVisitor
override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
structs.append(node)
return .skipChildren
}
}
let visitor = try Visitor(source: code)
visitor.structs.forEach {
print($0.identifier)
}
I found it after trial & error and reviewing of the API.
private func recursion(node: Syntax) -> String {
for child in node.children {
if let token = TokenSyntax(child), token.tokenKind == .structKeyword {
print ("yeah")
}
recursion(node: child)
}
return node.description
}
This approach to identify the kind of the token works, and the print statement will be reached. Again, I do wonder how the class diagram for SwiftSyntax would look like.

Use interface to pass data in Kotlin

I need to pass data to class from activity. I use interface, but i have problem with initialization.
My class:
class Methods {
fun processingResponse(finalMessage: String) {
var mcontext: Context? = null
var message : Message = Message()
var access = "Access Granted"
var out = "Logged"
var Stateconnect = false
var safetyCheck = 0
if (access in finalMessage) {
val msg = finalMessage.split("=", ":")
accessLevel = msg[0]
sessionId = msg[1].toInt()
safetyCheck = msg[2].toInt()
var namePlc = msg[3]
interfaceData.sendData("Connect")
//Stateconnect = true
} else if (out in finalMessage) {
interfaceData.sendData("Disconnect")
println("log out okey")
}
}}
My interface:
interface SendDataInterface {fun sendData(str: String )}
and My activity:
class LoginIn : AppCompatActivity(), SendDataInterface {
override fun sendData(str: String)
{
var handler = Handler(Looper.getMainLooper())
handler.post( Runnable() {
fun run() {
buttonChange(str)
}
})} fun buttonChange(str : String) {
if (str == "Connect") {
Connection.setBackgroundColor(Color.RED)
Connection.setText("Disconnection")
loadMaintenancePage()
} else if (str == "Disconnect") {
Connection.setBackgroundColor(Color.GREEN)
Connection.setText("Connection")
}
}
}
The error that i have is the interface isn't initialize.
How I can initialize the interface?
You have to create an instance of SendDataInterface in your class Methods.
var interfaceData:SendDtaInterface=Object:SendDtaInterface{
override fun sendData("Connect"){
}
}
interfaceData.sendDat("connect")enter code here

How to check if an XCTestCase test has failed

Is it possible to check within a running test if any of its XCTAsserts have failed? I have a test with a few assertions in a row, and I want to add some code afterward to perform a specific action if any of them failed:
class testClass : XCTestCase
{
func testSomething()
{
let someComputedValue1 = func1()
let someComputedValue2 = func2()
XCTAssertLessThanOrEqual(someComputedValue1, 0.5)
XCTAssertLessThanOrEqual(someComputedValue2, 0.2)
if anyOfTheAboveAssertionsFailed {
performAction()
}
}
}
The part I'd like tips on is that anyOfTheAboveAssertionsFailed condition without duplicating the comparisons to the hard-coded values.
While using your own assertion methods solves the PO's issue, it is cumbersome if you need to use several XCAssert-methods.
Another approach is to override continueAfterFailure. If there is no failure the property will not be requested. If there is one, it will.
class MyTest: XCTest {
private var hasFailed = false
override var continueAfterFailure: Bool {
get {
hasFailed = true
return super.continueAfterFailure
}
set {
super.continueAfterFailure = newValue
}
}
override func tearDown() {
if hasFailed { performAction() }
hasFailed = false
}
}
You could of course write a new function...
func assertLessThanOrEqual(value: Double, limit: Double) -> Bool {
XCTAssertLessThanOrEqual(value, limit)
return value <= limit
}
And then write your tests like...
var allGood = true
allGood = allGood && assertLessThanOrEqual(someComputedValue1, 0.5)
allGood = allGood && assertLessThanOrEqual(someComputedValue2, 0.2)
if !allGood {
performAction()
}