Creating a Transaction Handling System Using Kotlin and Android

Kacper Bąk
13 min readFeb 5, 2023

--

Photo by Nathan Dumlao on Unsplash

When working with Firebase databases, it is important to be aware of the various errors that may occur. This article will provide a step-by-step guide on how to fix a specific error related to retrieving data from a Firebase database. The code provided is an example of a situation where a user needs to get a receiverUid from the database. This article will provide guidance on how to troubleshoot this particular error and how to fix it.

For a very long time, I have been working on creating a system to handle transactions. To achieve this, I used the programming language Kotlin and the whole application was written for the Android ecosystem. I learned how the Kotlin Firebase solution worked, particularly how transactions and the Realtime Database service operated. The app consists of many different screens that allow users to send and receive payments for purchases or services. The app also uses various authentication methods such as logging in with Google or Facebook. Furthermore, I also wrote a script to perform automated transactions to make it easier to send and receive payments.

Below is an example code that contains the AddTransactionActivity activity written in Kotlin, this is the prototype and can be very well adapted to the user’s needs.

class AddTransactionActivity : AppCompatActivity() {

private lateinit var appBarConfiguration: AppBarConfiguration
private lateinit var binding: ActivityAddTransactionBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

binding = ActivityAddTransactionBinding.inflate(layoutInflater)
setContentView(binding.root)

setSupportActionBar(binding.toolbar)

// change scene to TransactionActivity
binding.toolbar.setNavigationOnClickListener {
finish()
}

// do transaction and store it in realtime database
binding.fab.setOnClickListener { view ->
// get data from edit text
val amount = binding.root.findViewById<AppCompatEditText>(R.id.amount).text.toString().toDouble()
val receiver = binding.root.findViewById<AppCompatEditText>(R.id.receiver).text.toString()

// check if user is providing amount
if (amount == 0.0) {
Snackbar.make(view, "Please enter amount", Snackbar.LENGTH_SHORT).show()
return@setOnClickListener
}

// check if user is providing receiver
if (receiver.isEmpty()) {
Snackbar.make(view, "Please enter receiver", Snackbar.LENGTH_SHORT).show()
return@setOnClickListener
}

// get current user
val user = FirebaseAuth.getInstance().currentUser

// get current time
val timestamp = Timestamp(System.currentTimeMillis())

// create transaction
val transaction = Transaction(amount, receiver, user!!.uid)

// save transaction to realtime database
Firebase.database.reference.child("users").child(user.uid).child("transactions").child(timestamp.toString()).setValue(transaction)

// change scene to TransactionActivity
finish()
}
}

private fun Transaction(amount: Double, receiver: String, sender: String): Transaction {
return Transaction(amount, receiver, sender)
}

override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.nav_host_fragment_content_add_transaction)
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
}
}

Generally, it seems to me that this AddTransactionActivity class could be divided into more classes or files in order to increase the objectivity of the project and facilitate later changes, project management, the implementation of additional business logic, or the maintenance and debugging of the code itself.

For example, one can separate the class responsible for performing the transaction, the class responsible for checking the user data, the class responsible for handling authentication or the class responsible for displaying the results of the transaction. Another way may be to separate the individual functions into separate files, e.g. the function checking the user data can be separated into a separate file, so that it is easier to review it.

So I wrote a new code, based on the lessons learned from this:

class AddTransactionActivity : AppCompatActivity() {

private lateinit var appBarConfiguration: AppBarConfiguration
private lateinit var binding: ActivityAddTransactionBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

binding = ActivityAddTransactionBinding.inflate(layoutInflater)
setContentView(binding.root)

setSupportActionBar(binding.toolbar)

// change scene to TransactionActivity
binding.toolbar.setNavigationOnClickListener {
finish()
}

// do transaction and store it in realtime database
binding.fab.setOnClickListener { view ->
doTransaction(view)
}
}

private fun doTransaction(view: View) {
// get data from edit text
val amount = binding.root.findViewById<AppCompatEditText>(R.id.amount).text.toString().toDouble()
val receiver = binding.root.findViewById<AppCompatEditText>(R.id.receiver).text.toString()

// check if user is providing amount
if (amount == 0.0) {
Snackbar.make(view, "Please enter amount", Snackbar.LENGTH_SHORT).show()
return
}

// check if user is providing receiver
if (receiver.isEmpty()) {
Snackbar.make(view, "Please enter receiver", Snackbar.LENGTH_SHORT).show()
return
}

// get current user
val user = FirebaseAuth.getInstance().currentUser

// get current time
val timestamp = Timestamp(System.currentTimeMillis())

// create transaction
val transaction = Transaction(amount, receiver, user!!.uid)

// save transaction to realtime database
Firebase.database.reference.child("users").child(user.uid).child("transactions").child(timestamp.toString()).setValue(transaction)

// change scene to TransactionActivity
finish()
}

private fun Transaction(amount: Double, receiver: String, sender: String): Transaction {
return Transaction(amount, receiver, sender)
}

override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.nav_host_fragment_content_add_transaction)
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
}
}

I have created a separate class called ReceiverChecker, which is responsible for checking whether the specified receiver exists. This class uses the Realtime Database to check that the specified receiver exists and that the user has permission to perform the transaction.

class ReceiverChecker {

private lateinit var receiverId: String

fun checkReceiver(receiver: String, onResult: (Boolean) -> Unit) {
receiverId = receiver
val rootRef = Firebase.database.reference

rootRef.child("users").child(receiver).addListenerForSingleValueEvent(object :
ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
val userExists = snapshot.exists()

if (userExists) {
onResult(true)
} else {
onResult(false)
}
}

override fun onCancelled(error: DatabaseError) {
// Do nothing
}
})
}
}

Then, in the AddTransactionActivity class, in the doTransaction() function, I added code that will launch an instance of ReceiverChecker and pass the appropriate data to the checkReceiver() function.

private fun doTransaction(view: View) {
// get data from edit text
val amount = binding.root.findViewById<AppCompatEditText>(R.id.amount).text.toString().toDouble()
val receiver = binding.root.findViewById<AppCompatEditText>(R.id.receiver).text.toString()

// check if user is providing amount
if (amount == 0.0) {
Snackbar.make(view, "Please enter amount", Snackbar.LENGTH_SHORT).show()
return
}

// check if user is providing receiver
if (receiver.isEmpty()) {
Snackbar.make(view, "Please enter receiver", Snackbar.LENGTH_SHORT).show()
return
}

// check if receiver exists
ReceiverChecker().checkReceiver(receiver) { result ->
if (result) {
// get current user
val user = FirebaseAuth.getInstance().currentUser

// get current time
val timestamp = Timestamp(System.currentTimeMillis())

// create transaction
val transaction = Transaction(amount, receiver, user!!.uid)

// save transaction to realtime database
Firebase.database.reference.child("users").child(user.uid).child("transactions").child(timestamp.toString()).setValue(transaction)

// change scene to TransactionActivity
finish()
} else {
Snackbar.make(view, "User doesn't exist", Snackbar.LENGTH_SHORT).show()
}
}
}
Screenshot of the application with running validation of whether a user exists in the database

However, as it later turned out, the receiver did not work as I had assumed. It was taking the UID as a parameter, while it should have been taking the Name, so that, the user had an easier time performing the transaction because the user’s UID should not be known. To this end, I refactored the ReceiverChecker class to improve its functionality.

class ReceiverChecker {

private lateinit var receiverName: String

fun checkReceiver(receiver: String, onResult: (Boolean) -> Unit) {
receiverName = receiver
val rootRef = Firebase.database.reference

rootRef.child("users").addListenerForSingleValueEvent(object :
ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
var userExists = false

snapshot.children.forEach {
if (it.child("name").value == receiverName) {
userExists = true
return@forEach
}
}

onResult(userExists)
}

override fun onCancelled(error: DatabaseError) {
// Do nothing
}
})
}
}

It also seems to me that, in the code below, it is possible to pull out the logic that has been thrown inside the checkReceiver to the outside, specifically that which relates to the database layer.

class AddTransactionActivity : AppCompatActivity() {

private lateinit var appBarConfiguration: AppBarConfiguration
private lateinit var binding: ActivityAddTransactionBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

binding = ActivityAddTransactionBinding.inflate(layoutInflater)
setContentView(binding.root)

setSupportActionBar(binding.toolbar)

// change scene to TransactionActivity
binding.toolbar.setNavigationOnClickListener {
finish()
}

// do transaction and store it in realtime database
binding.fab.setOnClickListener { view ->
doTransaction(view)
}
}

private fun doTransaction(view: View) {
// get data from edit text
val amount = binding.root.findViewById<AppCompatEditText>(R.id.amount).text.toString().toDouble()
val receiver = binding.root.findViewById<AppCompatEditText>(R.id.receiver).text.toString()
val description = binding.root.findViewById<AppCompatEditText>(R.id.description).text.toString()

// check if user is providing amount
if (amount == 0.0) {
Snackbar.make(view, "Please enter amount", Snackbar.LENGTH_SHORT).show()
return
}

// check if user is providing receiver
if (receiver.isEmpty()) {
Snackbar.make(view, "Please enter receiver", Snackbar.LENGTH_SHORT).show()
return
}

// check if receiver exists
ReceiverChecker().checkReceiver(receiver) { result ->
if (result) {
// get current user
val user = FirebaseAuth.getInstance().currentUser

// get current time
val timestamp = Timestamp(System.currentTimeMillis())

// create transaction
val transaction = Transaction(amount, receiver, user!!.uid, description, timestamp)

// create a new transaction in realtime database based on information provided by user
val database = Firebase.database
val myRef = database.getReference("transactions")
myRef.child(timestamp.toString()).setValue(transaction)

// change scene to TransactionActivity
finish()
} else {
Snackbar.make(view, "User doesn't exist", Snackbar.LENGTH_SHORT).show()
}
}
}


override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.nav_host_fragment_content_add_transaction)
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
}
}

We could extract the recipient checking logic into the `ReceiverChecker` class.

The `ReceiverChecker` class could look roughly like this:

class ReceiverChecker {
fun checkReceiver(receiver: String, callback: (Boolean) -> Unit) {
// get reference to the realtime database
val database = Firebase.database
val myRef = database.getReference("users")

// get user based on the receiver's username
myRef.child(receiver).addListenerForSingleValueEvent(object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
// check if user exists
val userExists = dataSnapshot.exists()

// invoke the callback function with the result
callback(userExists)
}

override fun onCancelled(databaseError: DatabaseError) {
Log.e("checkReceiver", "Error checking receiver: ${databaseError.message}")
}
})
}
}

And then, in the `AddTransactionActivity` class, it would be sufficient to call this method passing on the recipient and the return function to be executed:

    private fun doTransaction(view: View) {
// get data from edit text
val amount = binding.root.findViewById<AppCompatEditText>(R.id.amount).text.toString().toDouble()
val receiver = binding.root.findViewById<AppCompatEditText>(R.id.receiver).text.toString()
val description = binding.root.findViewById<AppCompatEditText>(R.id.description).text.toString()

// check if user is providing amount
if (amount == 0.0) {
Snackbar.make(view, "Please enter amount", Snackbar.LENGTH_SHORT).show()
return
}

// check if user is providing receiver
if (receiver.isEmpty()) {
Snackbar.make(view, "Please enter receiver", Snackbar.LENGTH_SHORT).show()
return
}

// check if receiver exists
ReceiverChecker().checkReceiver(receiver) { result ->
if (result) {
// get current user
val user = FirebaseAuth.getInstance().currentUser

// get current time
val timestamp = Timestamp(System.currentTimeMillis())

// create transaction
val transaction = Transaction(amount, receiver, user!!.uid, description, timestamp)

// create a new transaction in realtime database based on information provided by user
val database = Firebase.database
val myRef = database.getReference("transactions")
myRef.child(timestamp.toString()).setValue(transaction)

// change scene to TransactionActivity
finish()
} else {
Snackbar.make(view, "User doesn't exist", Snackbar.LENGTH_SHORT).show()
}
}
}

We could use the `Builder` design programming pattern to structure the code. We could create a `TransactionBuilder` class that would be used to create a `Transaction` object and extract the transaction creation logic into this class.

The `TransactionBuilder` class could look more or less like this:

class TransactionBuilder {
private var amount by Delegates.notNull<Double>()
private lateinit var receiver: String
private lateinit var description: String

fun setAmount(amount: Double): TransactionBuilder {
this.amount = amount
return this
}

fun setReceiver(receiver: String): TransactionBuilder {
this.receiver = receiver
return this
}

fun setDescription(description: String): TransactionBuilder {
this.description = description
return this
}

fun build(): Transaction {
// get current user
val user = FirebaseAuth.getInstance().currentUser

// get current time
val timestamp = Timestamp(System.currentTimeMillis())

// create transaction
return Transaction(amount, receiver, user!!.uid, description, timestamp)
}
}

`private lateinit var amount: Double` means that the `amount` variable must be initialized before we can use it. If the variable is not initialized beforehand, the application will stop with a `UninitialisedPropertyAccessException`.

In contrast, `private var amount by Delegates.notNull<Double>()` uses the `Delegates.notNull()` delegate, which ensures that the value of the `amount` variable cannot be null. Attempting to set the value of `amount` to `null` will result in an `IllegalStateException` exception. This means that you must set the value of the `amount` variable before use, but this does not have to be done at the time of declaration.

And then in the `AddTransactionActivity` class it would be sufficient to use this class to create a `Transaction` object:

private fun doTransaction(view: View) {
// get data from edit text
val amount = binding.root.findViewById<AppCompatEditText>(R.id.amount).text.toString().toDouble()
val receiver = binding.root.findViewById<AppCompatEditText>(R.id.receiver).text.toString()
val description = binding.root.findViewById<AppCompatEditText>(R.id.description).text.toString()

// check if user is providing amount
if (amount == 0.0) {
Snackbar.make(view, "Please enter amount", Snackbar.LENGTH_SHORT).show()
return
}

// check if user is providing receiver
if (receiver.isEmpty()) {
Snackbar.make(view, "Please enter receiver", Snackbar.LENGTH_SHORT).show()
return
}

// check if receiver exists
ReceiverChecker().checkReceiver(receiver) { result ->
if (result) {
// create a new transaction using TransactionBuilder
val transaction = TransactionBuilder()
.setAmount(amount)
.setReceiver(receiver)
.setDescription(description)
.build()

// create a new transaction in realtime database based on information provided by user
val database = Firebase.database
val myRef = database.getReference("transactions")
myRef.child(transaction.timestamp.toString()).setValue(transaction)

// change scene to TransactionActivity
finish()
} else {
Snackbar.make(view, "User doesn't exist", Snackbar.LENGTH_SHORT).show()
}
}
}

In writing this code, of course, I can recommend the Design Patterns from GoF, from which I have learned a great many lessons, on the basis of which I am able to write such clear code… And also https://refactoring.guru/design-patterns, it’s very handy and practical!

However, things still didn’t work quite as I had envisaged….

After some brief deduction and debugging, it became apparent that the code doesn’t quite work as it should. All the time the debugger was returning the user “gigachad” as non-existent, when in fact it existed in the database.

The model in the Firebase Realtime Database looks like this:

{
"address": "",
"balance": 0,
"name": "gigachad",
"phone": "123123222",
"uid": "EQxN7yWQlsbdSAdDeRLQuiAnCy73"
}

Here it looks like the checkReceiver function is not called with a valid argument. Let’s take a closer look.

The checkReceiver function expects the receiver argument to contain a unique user key. In this case, it is “gigachad”. However, in the Firebase Realtime Database, the user key is “EQxN7yWQlsbdSAdDeRLQuiAnCy73”, not “gigachad”. To call the checkReceiver function correctly, the correct user key must be used.

The code can be improved to call the checkReceiver function with a valid user key. An example of the corrected version could look like the following:

class ReceiverChecker {
fun checkReceiver(receiver: String, callback: (Boolean) -> Unit) {
// get reference to the realtime database
val database = Firebase.database
val myRef = database.getReference("users")

// get user based on the receiver's username
myRef.orderByChild("name").equalTo(receiver).addListenerForSingleValueEvent(object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
// check if user exists
val userExists = dataSnapshot.exists()

// invoke the callback function with the result
callback(userExists)
}

override fun onCancelled(databaseError: DatabaseError) {
Log.e("checkReceiver", "Error checking receiver: ${databaseError.message}")
}
})
}
}

Not to put too fine a point on it, it turned out that I could not store the transaction as a timestamp because it contained forbidden characters, as evidenced by the message:

com.google.firebase.database.DatabaseException: Invalid Firebase Database path: 2023–02–05 13:27:05.232. Firebase Database paths must not contain ‘.’, ‘#’, ‘$’, ‘[‘, or ‘]’

Here it looks like a transaction is created using Timestamp as the key in Firebase Realtime Database. Timestamps contain dots, which is not allowed in Firebase Realtime Database. To fix this error, you can use a different time representation, such as UNIX timestamp, to use as a key in the Firebase Realtime Database.

So I decided to replace the Unix timestamp with a timestamp as the key in the Firebase Realtime Database. This will avoid forbidden characters in the Firebase Realtime Database. An example of the revised version could look like the following:

private fun doTransaction(view: View) {
// get data from edit text
val amount = binding.root.findViewById<AppCompatEditText>(R.id.amount).text.toString().toDouble()
val receiver = binding.root.findViewById<AppCompatEditText>(R.id.receiver).text.toString()
val description = binding.root.findViewById<AppCompatEditText>(R.id.description).text.toString()

// check if user is providing amount
if (amount == 0.0) {
Snackbar.make(view, "Please enter amount", Snackbar.LENGTH_SHORT).show()
return
}

// check if user is providing receiver
if (receiver.isEmpty()) {
Snackbar.make(view, "Please enter receiver", Snackbar.LENGTH_SHORT).show()
return
}

// check if receiver exists
ReceiverChecker().checkReceiver(receiver) { result ->
if (result) {
// create a new transaction using TransactionBuilder
val transaction = TransactionBuilder()
.setAmount(amount)
.setReceiver(receiver)
.setDescription(description)
.build()

// create a new transaction in realtime database based on information provided by user
val database = Firebase.database
val myRef = database.getReference("transactions")
val unixTime = System.currentTimeMillis() / 1000L
myRef.child(unixTime.toString()).setValue(transaction)

// change scene to TransactionActivity
finish()
} else {
Snackbar.make(view, "User doesn't exist", Snackbar.LENGTH_SHORT).show()
}
}
}

And it worked!

Screenshot from the Firebase console

This is of course not entirely correct, as someone may send two transactions in the same UNIX time slot, although if we are sure that our application will not be so besieged and no one is paying for it, and we are doing it purely for educational purposes then it is possible to risk it.

Overall, it is not recommended to use UNIX time to generate transaction IDs. A better approach would be to use a combination of randomly generated numbers, UUIDs, or hashes to generate transaction IDs. This approach would make it much more difficult to guess or predict transaction IDs and would make it more secure.

In order to complete a transaction, code must be added that subtracts the specified ‘amount’ value from the ‘balance’ of the ‘sender’ and adds this ‘amount’ value to the ‘balance’ of the ‘receiver’. This logic should be added to the function responsible for creating the transactions, and this function should also check that the ‘sender’ has enough funds in their balance to complete the transaction. If they do, then the appropriate amount should be subtracted from the ‘sender’s balance and added to the ‘receiver’s balance.

So I took to completing the code to meet the assumptions listed.

    private fun doTransaction(view: View) {
// get data from edit text
val amount = binding.root.findViewById<AppCompatEditText>(R.id.amount).text.toString().toDouble()
val receiver = binding.root.findViewById<AppCompatEditText>(R.id.receiver).text.toString()
val description = binding.root.findViewById<AppCompatEditText>(R.id.description).text.toString()

// check if user is providing amount
if (amount == 0.0) {
Snackbar.make(view, "Please enter amount", Snackbar.LENGTH_SHORT).show()
return
}

// check if user is providing receiver
if (receiver.isEmpty()) {
Snackbar.make(view, "Please enter receiver", Snackbar.LENGTH_SHORT).show()
return
}

// check if receiver exists
ReceiverChecker().checkReceiver(receiver) { result ->
if (result) {
// get receiver uid from database
val database = Firebase.database
val receiverRef = database.getReference("users")
val receiverUidRef = receiverRef.orderByChild("name").equalTo(receiver)

receiverUidRef.addListenerForSingleValueEvent(object: ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
// get only value of uid from snapshot
val receiverUid = dataSnapshot.children.first().key.toString()

// check if sender has enough funds to complete the transaction
val senderUid = FirebaseAuth.getInstance().currentUser!!.uid
val senderRef = database.getReference("users/$senderUid/balance")
val receiverBalanceRef = database.getReference("users/$receiverUid/balance")

senderRef.addListenerForSingleValueEvent(object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
val senderBalance = dataSnapshot.getValue(Double::class.java)!!
if (senderBalance < amount) {
Snackbar.make(view, "Insufficient funds", Snackbar.LENGTH_SHORT).show()
return
}

// subtract amount from sender balance
senderRef.setValue(senderBalance - amount)

// add amount to receiver balance
receiverBalanceRef.addListenerForSingleValueEvent(object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
val receiverBalance = when (dataSnapshot.value) {
is Double -> dataSnapshot.value as Double
is Long -> (dataSnapshot.value as Long).toDouble()
else -> 0.0
}
receiverBalanceRef.setValue(receiverBalance + amount)
}

override fun onCancelled(error: DatabaseError) {
// Failed to read value
Log.w("Error", "Failed to read value.", error.toException())
}
})

// create a new transaction using TransactionBuilder
val transaction = TransactionBuilder()
.setAmount(amount)
.setReceiver(receiver)
.setDescription(description)
.build()

// create a new transaction in realtime database based on information provided by user
val myRef = database.getReference("transactions")
val unixTime = System.currentTimeMillis() / 1000L
myRef.child(unixTime.toString()).setValue(transaction)

// change scene to TransactionActivity
finish()
}

override fun onCancelled(error: DatabaseError) {
// Failed to read value
Log.w("Error", "Failed to read value.", error.toException())
}
})
}

override fun onCancelled(error: DatabaseError) {
// Failed to read value
Log.w("Error", "Failed to read value.", error.toException())
}
})
} else {
Snackbar.make(view, "User doesn't exist", Snackbar.LENGTH_SHORT).show()
}
}
}

Especially this snippet is cool:

val receiverBalance = when (dataSnapshot.value) {
is Double -> dataSnapshot.value as Double
is Long -> dataSnapshot.value.toDouble()
else -> 0.0
}

Overall, the process of creating a transaction handling system using Kotlin and the Android ecosystem was a very useful experience. I have learned many different things such as Firebase handling, objectivity in projects, as well as creating scripts to perform automated transactions.

Source code: https://github.com/53jk1/ExchangeableToken

--

--