Send push notifications in a social network Android app - Part 1
In order to follow this tutorial, you will need some experience with the Kotlin programming language. You will also need appropriate IDEs (IntelliJ IDEA and Android Studio are recommended)
This is part 1 of a 2 part tutorial. You can find part 2 here.
Introduction
Push notifications are the frontline interaction between a user and their connections within a social network, updating them when something has happened so they can pick up where they left off.
Push notifications for social networks include updates like hearts or comments on Instagram posts, dating matches on Tinder, or host-guest communication on Airbnb.
Setting up push notifications can be confusing and time-consuming. However, with Pusher’s Beams API, the process is a lot easier and faster.
In this article, we are going to build a simple Android application for event management, where users can express interest in a registered event. In part 2, we will then extend this application to send and receive push notifications whenever anything happens with these events.
Prerequisites
In order to follow along, you will need some experience with the Kotlin programming language, which we are going to use for both the backend and frontend of our application.
You will also need appropriate IDEs. We suggest IntelliJ IDEA and Android Studio.
Building the backend
The backend of our system is responsible for storing and providing the event details, and for triggering our push notifications anytime anything changes with them. We are going to build this in Kotlin using the Spring Boot framework, as this is a very quick way to get going for server-side Kotlin applications.
Head over to https://start.spring.io/ to create our project structure. We need to specify that we are building a Gradle project with Kotlin and Spring Boot 2.0.0 (Or newer if available at the time of reading), and we need to include the “Web” components:
The Generate Project button will give you a zip file containing our application structure. Unpack this somewhere. At any time, you can execute ./gradlew bootRun
to build and start your backend server running.
Firstly though, we need to add some dependencies. Open up the build.gradle
file and add the following to the dependencies
section:
runtime 'com.fasterxml.jackson.module:jackson-module-kotlin:2.9.2'
This is the Jackson module needed for serialising and deserialising Kotlin classes into JSON.
Now, build the project. This will ensure that all of the dependencies are downloaded and made available and that everything compiles and builds correctly:
$ ./gradlew build
BUILD SUCCESSFUL in 1s
5 actionable tasks: 5 up-to-date
Users and Friends APIs
Any Social Networking Application will need to have mechanisms to manage users and friends. In this example, we are not going to support creating new users or adding and removing friends. Instead, we are simply going to have a read-only API to support the rest of the application.
Note: we’re not going to be building a UI for the backend so we will have to interact with this using a REST client such as Postman.
The endpoints that we need are:
- GET /users/{id}
- GET /users/{id}/friends
Firstly, let’s create a class to represent the API payloads. We’re going to use the exact same class for both User and Friend, with the only difference being that the endpoint for listing friends returns a list of User classes instead of just one.
When the jackson-module-kotlin
dependency is included Spring automatically supports marshalling Kotlin Data classes to and from JSON, so all we need is to define this class:
data class User(
val id: String?,
val name: String
)
Next we need the Controller to work with this data:
@RestController
@RequestMapping("/users")
class UserController {
private val users = mutableListOf(
User(id = "john", name = "John"),
User(id = "paul", name = "Paul"),
User(id = "george", name = "George"),
User(id = "ringo", name = "Ringo")
)
private val friends = mapOf(
"john" to listOf("paul", "george", "ringo"),
"paul" to listOf("john", "george", "ringo"),
"george" to listOf("john", "paul", "ringo"),
"ringo" to listOf("john", "paul", "george")
)
@RequestMapping("/{id}")
fun getUser(@PathVariable("id") id: String) =
users.find { it.id == id }
?.let { ResponseEntity.ok(it) }
?: ResponseEntity.notFound().build()
@RequestMapping("/{id}/friends")
fun getFriends(@PathVariable("id") id: String) =
friends[id]?.map {friendId -> users.find { user -> user.id == friendId } }
?.filterNotNull()
?.let { ResponseEntity.ok(it) }
?: ResponseEntity.notFound().build()
}
Note: there is absolutely no effort put into authentication or authorization of this API. If you do this for real you will need to address that concern, but for our example application this is good enough.
In order to keep this simple, we’ve used a simple in-memory list inside of the controller for the data, and we’ve stored the API payload objects directly. In reality this would come from the database and would go through some translation layer to convert the DAO objects into the API objects - since it is unlikely they will be exactly the same structure - but for the sake of simplicity this gives us what we need.
As long as this is in the same package or a child package of the one containing your main Application class then Spring will automatically find it - because of the @RestController
annotation - and make it available. This then gives us most of the API functionality that we wanted straight away.
Events APIs
We now need some endpoints for interacting with events.
The endpoints that we need are:
- GET /events - To list all of the events that are known.
- GET /events/{id} - To get the details of a single event.
- POST /events - To create a new event.
- PUT /events/{id} - To update an existing event.
- DELETE /events/{id} - To delete an event.
- GET /events/{id}/interest - To get all of the users interested in an event.
- PUT /events/{id}/interest/{user} - To register interest in an event.
- DELETE /events/{id}/interest/{user} - To unregister interest in an event.
- POST /events/{id}/share - To share the event with another user
Note: Registering and unregistering interest in an event is done by providing the User ID in the URL. In reality the system would know which user you are by the authentication details provided, but since we aren’t implementing authentication we’ve got to do something else instead.
This seems like a lot, but we’re not going to do anything complicated with them. Instead, as before, we’re going to use a simple list to contain the events, and the event data will directly be the API payload.
Firstly, let’s create our Event API payload. Create a new class called Event
as follows:
data class Event(
val id: String?,
val name: String,
val description: String,
val start: Instant
)
Next we need the Controller to work with this data:
@RestController
@RequestMapping("/events")
class EventController {
private val events = mutableListOf(
Event(
id = "xmas",
name = "Christmas",
description = "It's the most wonderful time of the year",
start = Instant.parse("2018-12-25T00:00:00Z")
)
)
@RequestMapping
fun getEvents() = events
@RequestMapping("/{id}")
fun getEvent(@PathVariable("id") id: String) =
events.find { it.id == id }
?.let { ResponseEntity.ok(it) }
?: ResponseEntity.notFound().build()
@RequestMapping(method = [RequestMethod.POST])
fun createEvent(@RequestBody event: Event): Event {
val newEvent = Event(
id = UUID.randomUUID().toString(),
name = event.name,
description = event.description,
start = event.start
)
events.add(newEvent)
return newEvent
}
@RequestMapping(value = ["/{id}"], method = [RequestMethod.DELETE])
fun deleteEvent(@PathVariable("id") id: String) {
events.removeIf { it.id == id }
}
@RequestMapping(value = ["/{id}"], method = [RequestMethod.PUT])
fun updateEvent(@PathVariable("id") id: String, @RequestBody event: Event): ResponseEntity<Event>? {
return if (events.removeIf { it.id == id }) {
val newEvent = Event(
id = id,
name = event.name,
description = event.description,
start = event.start
)
events.add(newEvent)
ResponseEntity.ok(newEvent)
} else {
ResponseEntity.notFound().build()
}
}
@RequestMapping(value = ["/{id}/share"], method = [RequestMethod.POST])
fun shareEvent(@PathVariable("id") event: String, @RequestBody friends: List<String>) {
}
}
Note that there’s no functionality here for sharing events. That is because the only thing it does is to send push notifications, which will be covered in the second article.
Next we want a controller to allow users to show interest in events. This is going to be based on simple in-memory data types again for simplicity sake. Add the following to the same Controller class:
private val interest: MutableMap<String, MutableSet<String>> = mutableMapOf()
@RequestMapping("/{id}/interest")
fun getInterest(@PathVariable("id") event: String) =
interest.getOrElse(event) {
mutableSetOf()
}
@RequestMapping(value = ["/{id}/interest/{user}"], method = [RequestMethod.PUT])
fun registerInterest(@PathVariable("id") event: String, @PathVariable("user") user: String) {
val eventInterest = interest.getOrPut(event) {
mutableSetOf()
}
eventInterest.add(user)
}
@RequestMapping(value = ["/{id}/interest/{user}"], method = [RequestMethod.DELETE])
fun unregisterInterest(@PathVariable("id") event: String, @PathVariable("user") user: String) {
val eventInterest = interest.getOrPut(event) {
mutableSetOf()
}
eventInterest.remove(user)
}
At this point, all of our API methods can be called and will work exactly as expected. You can use a tool like cURL or Postman to test them out for yourselves.
Building the Android application
The frontend Android application will also be built in Kotlin, using Android Studio. To start, open up Android Studio and create a new project, entering some appropriate details and ensuring that you select the Include Kotlin support option.
Then on the next screen, ensure that you select support for Phone and Tablet using at least API 16:
Ensure that an Empty Activity is selected:
And change the Activity Name to “LoginActivity”:
Then add the following to the dependencies
section of the App level build.gradle
:
compile 'com.loopj.android:android-async-http:1.4.9'
compile 'com.google.code.gson:gson:2.2.4'
Finally, we need to add some permissions to our application. Open up the AndroidManifest.xml
file and add the following immediately before the <application>
tag:
<uses-permission android:name="android.permission.INTERNET"/>
Remembering the logged in user
For the application to work, we need to know which user we have logged in as. We are going to do this by extending the standard Application
class to add our own data value for the username.
To do this, first create a new class called EventsApplication
as follows:
class EventsApplication : Application() {
var username: String? = null
}
Then update the AndroidManifest.xml
file to reference it:
<application
android:name=".EventsApplication"
From now on, any of our Activity
classes will see this.application
as being the same instance of EventsApplication
, and any changes we make to EventsApplication.username
will persist between activities.
User login screen
The first thing we want to create in our Android application is the User Login screen. For our application, this is a trivial case of entering a username. Remember that we are not implementing authentication or authorization so we are are not going to request passwords, and we are not going to remember which user you are logged in as.
Update activity_login.xml
to contain the following:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical|fill_horizontal"
android:orientation="vertical"
tools:layout_editor_absoluteX="8dp"
tools:layout_editor_absoluteY="8dp">
<TextView
android:id="@+id/userNameLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Username:" />
<EditText
android:id="@+id/userNameInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="textVisiblePassword" />
<Button
android:id="@+id/loginButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Login"
android:onClick="onClickLogin" />
</LinearLayout>
Notice that we have an entry in the Button
for android:onClick
. This sets us up to have a handler for clicking on the button straight away, so let’s make use of this.
Add the following to LoginActivity
:
fun onClickLogin(v: View) {
val usernameInput = findViewById<EditText>(R.id.userNameInput)
val username = usernameInput.text.toString()
if (username.isBlank()) {
Toast.makeText(this, "No username entered!", Toast.LENGTH_LONG).show()
} else {
(this.application as EventsApplication).username = username
startActivity(Intent(this, EventsListActivity::class.java))
}
}
In the case that a username was not entered, we display a Toast message informing the user of this fact. If they have, we store it onto our EventsApplication
class and transition to the EventsListActivity
that we are about to write.
Listing events
Once we have logged in, we can show the list of events in the system. For this, we will create a new Activity containing the list of events.
Create a new resource layout file called activity_eventslist.xml
as follows:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#fff">
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TableLayout
android:layout_marginTop="10dp"
android:id="@+id/table_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TableRow
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:id="@+id/table_row1"
android:padding="10dp">
<TextView
android:id="@+id/name"
android:fontFamily="serif"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_weight="1"
android:textColor="#000"
android:text="Name"/>
<TextView
android:id="@+id/date"
android:textColor="#000"
android:fontFamily="serif"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_weight="1"
android:text="Date"/>
</TableRow>
<View
android:layout_height="3dip"
android:layout_width="match_parent"
android:background="#ff0000"/>
</TableLayout>
<ListView
android:id="@+id/records_view"
android:layout_width="match_parent"
android:layout_height="500dp"
android:layout_marginTop="16dp">
</ListView>
</LinearLayout>
</ScrollView>
</LinearLayout>
This layout gives us a table layout to represent our header and a list view in which we are going to render the individual events that are currently available.
Next create a new class called EventsListActivity
as follows:
class EventsListActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_eventslist)
}
}
And finally add an entry to AndroidManifest.xml
inside the application
tag to register the new Activity:
<activity android:name=".EventsListActivity" />
Now we want to populate our list view from our backend data. The first thing we need is a class to represent the data in each row. Create a new class called Event
as follows:
data class Event(
val id: String,
val name: String,
val description: String,
val start: String
)
You will notice that this is almost the same as the equivalent class in the backend. The differences are that the ID is not nullable because every event is guaranteed to have an ID here; and the start time is a string, because Android runs on Java 6 and the Instant
class is not available here.
Now we need a means to convert the Event
data into a record to display in our list view. For this, create a new EventAdapter
class:
class EventAdapter(private val recordContext: Context) : BaseAdapter() {
var records: List<Event> = listOf()
set(value) {
field = value
notifyDataSetChanged()
}
override fun getView(i: Int, view: View?, viewGroup: ViewGroup): View {
val theView = if (view == null) {
val recordInflator = recordContext.getSystemService(Activity.LAYOUT_INFLATER_SERVICE) as LayoutInflater
val theView = recordInflator.inflate(R.layout.record, null)
val newEventViewHolder = EventViewHolder(
theView.findViewById(R.id.event_name),
theView.findViewById(R.id.event_date)
)
theView.tag = newEventViewHolder
theView
} else {
view
}
val eventViewHolder = theView.tag as EventViewHolder
val event = getItem(i)
eventViewHolder.nameView.text = event.name
eventViewHolder.dateView.text = event.start
eventViewHolder.id = event.id
return theView
}
override fun getItem(i: Int) = records[i]
override fun getItemId(i: Int) = 1L
override fun getCount() = records.size
}
data class EventViewHolder(
val nameView: TextView,
val dateView: TextView
) {
var id: String? = null
}
Amongst other things, this is responsible for creating and populating a new view that we will describe soon. This view is then populated with data from the appropriate event object, as held by our new EventViewHolder
class.
Next we need to describe our view. For this, create a new layout file called event.xml
as follows:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal" android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/event_name"
android:fontFamily="serif"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_weight="1"
android:textColor="#000"
android:text="Name"/>
<TextView
android:id="@+id/event_date"
android:textColor="#000"
android:fontFamily="serif"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_weight="1"
android:text="Date"/>
</LinearLayout>
Now we want to retrieve the list of events from our backend service. For this article we will do this on startup for simplicity. Open up EventsListActivity
, and add the following. Firstly we need a constant to define the URL to retrieve the events from:
private val EVENTS_ENDPOINT = "http://10.0.2.2:8080/events"
Note: The IP Address “10.0.2.2” is used when running on an Android emulator to refer to the host machine. In reality this should be the correct address of the backend server.
Next add a new field to the EventsListActivity
class:
private lateinit var recordAdapter: EventAdapter
Create a new function to refresh the events list:
private fun refreshEventsList() {
val client = AsyncHttpClient()
client.get(EVENTS_ENDPOINT, object : JsonHttpResponseHandler() {
override fun onSuccess(statusCode: Int, headers: Array<out Header>, response: JSONArray) {
super.onSuccess(statusCode, headers, response)
runOnUiThread {
val events = IntRange(0, response.length() - 1)
.map { index -> response.getJSONObject(index) }
.map { obj ->
Event(
id = obj.getString("id"),
name = obj.getString("name"),
description = obj.getString("description"),
start = obj.getString("start")
)
}
recordAdapter.records = events
}
}
})
}
Now add this to the onCreate
method:
recordAdapter = EventAdapter(this)
val recordsView = findViewById<View>(R.id.records_view) as ListView
recordsView.setAdapter(recordAdapter)
refreshEventsList()
At this point, running the Android application would show the list of events as they are in the backend server, displaying the event name and start time in the list.
The next thing we want is to add a menu to this screen. That will act as our route to creating new events, and to allowing the user to manually reload the events list (as a stand-in for automatically refreshing the list).
Create a new resource file eventslist.xml
under res/menu
:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:icon="@android:drawable/ic_input_add"
android:title="New Event"
app:showAsAction="ifRoom"
android:onClick="onClickNewEvent" />
<item
android:icon="@android:drawable/ic_popup_sync"
android:title="Refresh"
app:showAsAction="ifRoom"
android:onClick="onClickRefresh" />
</menu>
Then update the EventsListActivity
class to inflate this menu:
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.eventslist, menu)
return true
}
We also need to add handlers for our menu items:
fun onClickNewEvent(v: MenuItem) {
startActivity(Intent(this, CreateEventsActivity::class.java))
}
fun onClickRefresh(v: MenuItem) {
refreshEventsList()
}
The onClickRefresh
handler uses our already existing refreshEventsList
method, and the onClickNewEvent
handler will start an as-yet-unwritten activity.
Creating new events
Next we want the ability to create new events in the system. This will be a simple form allowing the user to enter a name, description and start time for the event, and will then send this to the backend.
Create a new resource layout file called activity_createevent.xml
as follows:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical|fill_horizontal"
android:orientation="vertical"
tools:layout_editor_absoluteX="8dp"
tools:layout_editor_absoluteY="8dp">
<TextView
android:id="@+id/nameLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Event Name:" />
<EditText
android:id="@+id/nameInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="textCapWords|textAutoCorrect" />
<TextView
android:id="@+id/descriptionLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Description:" />
<EditText
android:id="@+id/descriptionInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="textCapSentences|textAutoCorrect|textAutoComplete" />
<TextView
android:id="@+id/startLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start Time:" />
<EditText
android:id="@+id/startInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10" />
<Button
android:id="@+id/createButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="onClickCreate"
android:text="Create Event" />
</LinearLayout>
This layout gives us a very simple form and a button that will be used to actually create the event.
Note: it does require us to enter the start time in the very specific format, which is not good user experience. Adding a Date/Time picker is left as an exercise to the reader.
Next create a new class called CreateEventsActivity
as follows:
class CreateEventsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_createevent)
}
fun onClickCreate(v: View) {
}
}
And finally add an entry to AndroidManifest.xml
inside the application
tag to register the new Activity:
<activity android:name=".CreateEventsActivity" android:windowSoftInputMode="adjustResize"/>
Note the new
android:windowSoftInputMode
attribute. This tells Android to resize the activity whenever the keyboard is displayed, rather than displaying the keyboard over the top of it.
Now we just need to actually create the event. Open up CreateEventsActivity
, and add the following. Firstly we need a constant to define the URL to send the event details to:
private val EVENTS_ENDPOINT = "http://10.0.2.2:8080/events"
Then we need to implement our onClickCreate
method:
fun onClickCreate(v: View) {
val nameInput = findViewById<EditText>(R.id.nameInput)
val descriptionInput = findViewById<EditText>(R.id.descriptionInput)
val startInput = findViewById<EditText>(R.id.startInput)
val name = nameInput.text.toString()
val description = descriptionInput.text.toString()
val start = startInput.text.toString()
if (name.isBlank()) {
Toast.makeText(this, "No event name entered!", Toast.LENGTH_LONG).show()
} else if (start.isBlank()) {
Toast.makeText(this, "No start time entered!", Toast.LENGTH_LONG).show()
} else {
val transitionIntent = Intent(this, EventsListActivity::class.java)
val client = AsyncHttpClient()
val request = JSONObject(mapOf(
"name" to name,
"description" to description,
"start" to start
))
client.post(applicationContext, EVENTS_ENDPOINT, StringEntity(request.toString()), "application/json", object : JsonHttpResponseHandler() {
override fun onSuccess(statusCode: Int, headers: Array<out Header>, response: JSONObject) {
startActivity(transitionIntent)
}
})
}
}
This will send the appropriate HTTP Request to our server, providing the event details, and then - on a successful response - direct the user back to the Events List. By the time our user gets there, the event will have been created and it will automatically appear in the list.
Viewing event details
The final part of the UI is to be able to see the full details of an event, rather than just the list. This will include the description, the number of users that are interested, and whether or not the current user is on the list. It will also give the ability to register or remove interest in the event.
Create a new resource layout file called activity_viewevent.xml
as follows:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical|fill_horizontal"
android:orientation="vertical"
tools:layout_editor_absoluteX="8dp"
tools:layout_editor_absoluteY="8dp">
<TextView
android:id="@+id/nameLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Event Name:" />
<TextView
android:id="@+id/nameValue"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="" />
<TextView
android:id="@+id/descriptionLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Description:" />
<TextView
android:id="@+id/descriptionValue"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="" />
<TextView
android:id="@+id/startLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start Time:" />
<TextView
android:id="@+id/startValue"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="" />
<TextView
android:id="@+id/numberInterestLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="No. of Interested Users:" />
<TextView
android:id="@+id/numberInterestValue"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="" />
<Button
android:id="@+id/interestedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="onClickInterested"
android:text="Interested" />
<Button
android:id="@+id/disinterestedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="onClickDisinterested"
android:text="Not Interested" />
</LinearLayout>
Next create a new class called ViewEventActivity
as follows:
class ViewEventActivity : AppCompatActivity() {
private lateinit var eventId: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_viewevent)
eventId = intent.getStringExtra("event")
refreshEventDetails()
}
private fun refreshEventDetails() {
Log.v("ViewEvent", eventId)
}
fun onClickInterested(v: View?) {
}
fun onClickDisinterested(v: View) {
}
}
And register it in the AndroidManifest.xml
file:
<activity android:name=".ViewEventActivity" />
Then we need to be able to get to this new activity by clicking on an event in the list. Update EventsListActivity
.
Firstly, add a superclass of AdapterView.OnItemClickListener
and then implement it by adding the following method:
override fun onItemClick(parent: AdapterView<*>, view: View, position: Int, id: Long) {
val eventViewHolder = view.tag as EventViewHolder
val intent = Intent(this, ViewEventActivity::class.java)
intent.putExtra("event", eventViewHolder.id)
startActivity(intent)
}
Then register this by adding the following to onCreate
:
recordsView.onItemClickListener = this
We are using this to pass some extra data in the Intent to display an Activity - namely the ID of the event that is being displayed. We can now use that to load the event data and display it to the user.
For that, let’s implement the refreshEventDetails
method of ViewEventActivity
.
private fun refreshEventDetails() {
val client = AsyncHttpClient()
client.get(EVENTS_ENDPOINT + eventId, object : JsonHttpResponseHandler() {
override fun onSuccess(statusCode: Int, headers: Array<out Header>, response: JSONObject) {
super.onSuccess(statusCode, headers, response)
val nameDisplay = findViewById<TextView>(R.id.nameValue)
val descriptionDisplay = findViewById<TextView>(R.id.descriptionValue)
val startDisplay = findViewById<TextView>(R.id.startValue)
val name = response.getString("name")
val description = response.getString("description")
val start = response.getString("start")
runOnUiThread {
nameDisplay.text = name
descriptionDisplay.text = description
startDisplay.text = start
}
}
})
client.get(EVENTS_ENDPOINT + eventId + "/interest", object : JsonHttpResponseHandler() {
override fun onSuccess(statusCode: Int, headers: Array<out Header>, response: JSONArray) {
super.onSuccess(statusCode, headers, response)
val numberInterestedDisplay = findViewById<TextView>(R.id.numberInterestValue)
val interestedButton = findViewById<Button>(R.id.interestedButton)
val notInterestedButton = findViewById<Button>(R.id.disinterestedButton)
val numberInterested = response.length().toString()
val imInterested = IntRange(0, response.length() - 1)
.map { index -> response.getString(index) }
.contains((application as EventsApplication).username)
runOnUiThread {
numberInterestedDisplay.text = numberInterested
if (imInterested) {
interestedButton.visibility = View.GONE
notInterestedButton.visibility = View.VISIBLE
} else {
interestedButton.visibility = View.VISIBLE
notInterestedButton.visibility = View.GONE
}
}
}
})
}
This is a busy method, but essentially it is making two API calls - the first to get the event details, the second to get the list of people interested in the event - and then updating the UI to display these details.
Finally, let’s allow the user to express interest in the event. This is done by implementing the onClickInterested
and onClickDisinterested
buttons:
fun onClickInterested(v: View?) {
val client = AsyncHttpClient()
val username = (application as EventsApplication).username
client.put(EVENTS_ENDPOINT + eventId + "/interest/" + username, object : AsyncHttpResponseHandler() {
override fun onSuccess(statusCode: Int, headers: Array<out Header>?, responseBody: ByteArray?) {
runOnUiThread {
refreshEventDetails()
}
}
override fun onFailure(statusCode: Int, headers: Array<out Header>?, responseBody: ByteArray?, error: Throwable?) {
runOnUiThread {
refreshEventDetails()
}
}
})
}
fun onClickDisinterested(v: View) {
val client = AsyncHttpClient()
val username = (application as EventsApplication).username
client.delete(EVENTS_ENDPOINT + eventId + "/interest/" + username, object : AsyncHttpResponseHandler() {
override fun onSuccess(statusCode: Int, headers: Array<out Header>?, responseBody: ByteArray?) {
runOnUiThread {
refreshEventDetails()
}
}
override fun onFailure(statusCode: Int, headers: Array<out Header>?, responseBody: ByteArray?, error: Throwable?) {
runOnUiThread {
refreshEventDetails()
}
}
})
}
We do no handling of the response at all here, simply using the callback to refresh the view details. This is far from ideal but is good enough for now.
At this point, you can click on the buttons and see the interest levels changing.
Sharing an event with friends
The last part of our UI is to be able to share an event with friends. This will use a dialog from the View Event page and send the request to our server, which will in turn broadcast it on to the appropriate users.
Firstly, we want a dialog to display to the user allowing them to select the friends to share the event with. Create a new ShareEventDialog
class:
class ShareEventDialog : DialogFragment() {
private val EVENTS_ENDPOINT = "http://10.0.2.2:8080/events/"
lateinit var event: String
lateinit var friends: List<Friend>
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val names = friends.map { it.name }
val selected = mutableSetOf<String>()
return AlertDialog.Builder(activity)
.setTitle("Share")
.setMultiChoiceItems(names.toTypedArray(), null) { dialog, which, isChecked ->
val friend = friends[which]
if (isChecked) {
selected.add(friend.id)
} else {
selected.remove(friend.id)
}
}
.setPositiveButton("Share") { dialog, which ->
Log.v("ShareEventDialog", "Sharing with: " + selected)
val client = AsyncHttpClient()
val request = JSONArray(selected)
client.post(null,EVENTS_ENDPOINT + event + "/share", StringEntity(request.toString()), "application/json",
object : JsonHttpResponseHandler() {
})
}
.setNegativeButton("Cancel") { dialog, which -> }
.create()
}
}
This does all of the work of displaying our dialog and sending the request to the server on success.
Next, we want to create a menu with the Share button on it. For this, create a new menu resource called view.xml
as follows:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:icon="@android:drawable/ic_menu_share"
android:title="Share"
app:showAsAction="ifRoom"
android:onClick="onClickShare" />
</menu>
Then we need to actually display it. Add the following to ViewEventActivity
:
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.view, menu)
return true
}
And we need to implement the onClickShare
method.
We need to obtain our list of friends from the API that we can share with. We’ll create a Friend
class to represent each friend:
data class Friend(
val id: String,
val name: String
)
Then obtain the actual list of friends. Add the following field to ViewEventActivity
:
private val USERS_ENDPOINT = "http://10.0.2.2:8080/users/"
And then an implementation of onClickShare
:
fun onClickShare(v: MenuItem) {
val client = AsyncHttpClient()
client.get(USERS_ENDPOINT + (application as EventsApplication).username + "/friends", object : JsonHttpResponseHandler() {
override fun onSuccess(statusCode: Int, headers: Array<out Header>?, response: JSONArray) {
super.onSuccess(statusCode, headers, response)
val friends = IntRange(0, response.length() - 1)
.map { index -> response.getJSONObject(index) }
.map { obj ->
Friend(
id = obj.getString("id"),
name = obj.getString("name")
)
}
runOnUiThread {
val dialog = ShareEventDialog()
dialog.event = eventId
dialog.friends = friends
dialog.show(supportFragmentManager, "ShareEventDialog")
}
}
})
}
Conclusion
So far, we have a very simple application that can be used as a basic social events platform.
The full source for the entire application is available on Github.
This is part 1 of a 2 part tutorial. You can find part 2 here.
12 April 2018
by Graham Cox