Building Kotlin Mobile App: Editing, Creating Data

Here in Part Two of our three-part series, we continue to build on the fundamentals of Android development with the Salesforce Mobile SDK. In Part One, we covered project setup and creating a layout that fetches data from Salesforce, using Kotlin as our programming language of choice.

Before we continue towards developing a complete mobile synchronization strategy, we’ll first build out our mobile app to allow editing and creating data on our Salesforce org.

Editing Data

With the view established, let’s see how we can edit the data. Ideally, we want to maintain the same list format, but make it such that if a user taps on a name, they can edit it. Those changes should then be sent back to Salesforce.

To accomplish this, we need to do a few things. First, we need to override the default list behavior to make items editable. Then, after a record edit is complete, we need to make a call to the Salesforce API to update the data.

As before, let’s start with the layout. Create a file in app/res/layout called broker_item.xmland paste these lines into it:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   android:orientation="vertical"
   android:layout_width="fill_parent"
   android:layout_height="fill_parent">

   <EditText android:id="@+id/broker_field"
       android:layout_width="fill_parent"
       android:layout_height="wrap_content"
       />
</LinearLayout>

Next, create a file in app/java/com.example.sfdc called BrokerListAdapter.ktand paste this into it:

package com.example.sfdc

import android.content.Context;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import com.salesforce.androidsdk.rest.BrokerItemBinding
import com.salesforce.androidsdk.rest.ApiVersionStrings
import com.salesforce.androidsdk.rest.RestClient
import com.salesforce.androidsdk.rest.RestRequest
import com.salesforce.androidsdk.rest.RestResponse

class BrokerListAdapter : ArrayAdapter<String> {
   internal var context: Context
   Private lateinit var brokerItemBinding: BrokerItemBinding
   private var client: RestClient? = null
   private val nameToId: MutableMap<String, String> = mutableMapOf()

   constructor(context: Context) : super(context, R.layout.broker_item, ArrayList<String>(0)) {
       this.context = context;
   }

   fun setClient(client: RestClient?) {
       this.client = client
   }

   fun map(name: String, id: String) {
       this.nameToId.put(name, id)
   }

   override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
       val inflater = context
           .getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
       brokerItemBinding = BrokerItemBinding.inflate(inflater)
       val rowView: View = inflater.inflate(R.layout.broker_item, parent, false)
       val brokerField = brokerItemBinding.brokerField
       var item = getItem(position)
       var brokerId = nameToId.get(item)
       val fields: MutableMap<String, String> =  mutableMapOf()

       brokerField.setText(item)
       brokerField.addTextChangedListener(object : TextWatcher {
           override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
           override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
           override fun afterTextChanged(s: Editable) {
               item = brokerField.text.toString()

               fields.put("Name", item!!)
               val restRequest = RestRequest.getRequestForUpdate(ApiVersionStrings.getVersionNumber(context), "Broker__c", brokerId,
                   fields as Map<String, Any>?
               )
               client?.sendAsync(restRequest, object : RestClient.AsyncRequestCallback {
                   override fun onSuccess(request: RestRequest, result: RestResponse) {}

                   override fun onError(exception: Exception) {}
               })
           }
       })
       return rowView
   }
}

In the Android ecosystem, an adapter defines the functionality of a UI element. Here, we’ve created a new adapter that overrides certain behaviors of ArrayAdapter. We’ve added a function called setClientwhich reuses the same Salesforce API client that we need to populate the list over in MainActivity. The map function associates a broker’s name with their custom object ID. The reason why will be apparent soon.

getView is where the bulk of the activity happens. The most important line is the one dealing with RestRequest.getRequestForUpdate. The client calls this to update Salesforce data; it requires the name of the custom object, its ID, and the value to replace (in this case, the name). This event occurs when afterTextChanged fires; that is, after the user has finished updating the broker’s name. In a real production environment, you need to check the status codes for potential errors from the API, but for the sake of brevity, we’ve omitted any type of response check.

In MainActivity, we need to make use of this adapter. First, at the top of the class definition, change listAdapter to be a BrokerListAdapter instead of an ArrayAdapterlike so:

class MainActivity : SalesforceActivity() {

   private var client: RestClient? = null
   private var listAdapter: ArrayAdapter<String>? = null
   private lateinit var listAdapter: BrokerListAdapter
    ...

The lateinit modifier allows you to initialize a non-null type outside of the constructor. Next, replace the two onResume functions with these:

    override fun onResume() {
        // Hide everything until we are logged in
        mainViewBinding.root.visibility = View.INVISIBLE

        // Create list adapter
        listAdapter = BrokerListAdapter(this)
        mainViewBinding.brokersList.adapter = listAdapter

        super.onResume()
    }

    override fun onResume(client: RestClient) {
        // Keeping reference to rest client
        this.client = client
        listAdapter.setClient(client)

        // Show everything
        mainViewBinding.root.visibility = View.VISIBLE
        sendRequest("SELECT Name, Id FROM Broker__c")
    }

Lastly, we need to keep track of the broker’s real name along with their record Id. To do that, we can simply store it in the dictionary the BrokerListAdapter maintains. In sendRequestreplace the for loop there with this one:

for (i in 0..records.length() - 1) {
   listAdapter.add(records.getJSONObject(i).getString("Name"))
   listAdapter.map(records.getJSONObject(i).getString("Name"), records.getJSONObject(i).getString("Id"))
}

Go ahead and launch the app, and your list items will be editable. Make an edit, and then get ready, because things are about to get wild. Go back to your scratch org, and click on the Brokers tab in the Dreamforce app. Your edits should be reflected here on the Salesforce platform!

Adding Data

We can edit records, but what if we need to add a new broker? The format for this is pretty similar to the logic we used for fetching and editing records. Let’s quickly go through the steps.

Open up main.xml and paste these lines right before the ListView:

<LinearLayout android:orientation="horizontal"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:background="?android:colorBackground"
  android:layout_marginStart="10dp"
  android:layout_marginEnd="10dp"
  android:layout_marginTop="10dp"
  android:layout_marginBottom="10dp">

  <Button
     android:id="@+id/add_broker"
     android:layout_width="0dp"
     android:layout_height="47dp"
     android:onClick="onAddBrokerClick"
     android:text="Add broker"
     android:background="?android:colorPrimary"
     android:textColor="?attr/sfColorSecondary"
     android:layout_gravity="center"
     android:layout_weight="1"
     android:layout_marginEnd="10dp"/>
</LinearLayout>

Here, we’ve added a button that will call a function called onAddBrokerClick whenever it is pressed. In MainActivitywe’ll define that method:

fun onAddBrokerClick(v: View) {
   listAdapter.add("New Broker")
   var fields: Map<String, String> = mapOf("name" to "New Broker",
                                            "Title__c" to "Junior Broker",
                                            "Phone__c" to "555-555-1234",
                                            "Mobile_Phone__c" to  "555-555-1234",
                                            "Email__c" to "todo@salesforce.com",
                                            "Picture__c" to "https://cdn.iconscout.com/icon/free/png-256/salesforce-282298.png")
   val restRequest = RestRequest.getRequestForUpsert(ApiVersionStrings.getVersionNumber(this), "Broker__c", "Id", null, fields)

   client?.sendAsync(restRequest, object : AsyncRequestCallback {
       override fun onSuccess(request: RestRequest, result: RestResponse) {}
       override fun onError(exception: Exception) {}
   })
}

Yes, that’s it! Those fields we’ve defined relate directly to the custom object. By setting Id to null, we’re telling the API that this is a new record. If you add a new row, edit it, and then head back to your scratch org, you’ll see the new data appear online.

To Be Continued…

Building on top of what we accomplished last time, in this post, we were able to edit and add broker data in our mobile app, and then see it reflected in Salesforce. Pretty neat!

However, this is only a one-way data change: from the app to the org. What about sending data from the org to the app? In addition, how can we handle network issues that are unique to mobile phones, such as connectivity issues, or multiple people changing fields at the same time? We’ll address these very real concerns in our concluding post!

.

Leave a Comment