Build an Android poll app with push notifications
You will need Android Studio 3+ and virtualenv installed on your machine. Basic knowledge of Android development is required.
The web has become so dynamic that it’s weird to have to refresh anything anymore. We expect instant feedback from whatever application we are using and whatever action we are taking on the application.
Polls adopt realtime technologies to give the owners live updates. This has become a major feature in top social media platforms and it is most essential when you need to perform quick surveys. Popular services like Twitter have adopted polls as a part of their services and it works well to gather user sentiments and thoughts.
In this tutorial, you will learn how to build a realtime poll. We will be using Kotlin, Flask and Pusher Channels. By the time we are done, we will have an application that looks like this:
Prerequisites
In other to follow this tutorial, you need the following:
- Android Studio. Version >= 3.x is recommended. Download here.
- Flask - you will be guided on how to install it in the post.
- Virtualenv installed on your machine.
- Introductory knowledge of Kotlin. Here are some resources.
- Pusher Beams and Pusher Channels instance.
Setting up your Android application
Create a new project and follow the wizard to set it up. Name your app RealtimePolls
. Enter your company‘s domain name. The company domain affects the package name. We will set the domain to com.example
and the package name to com.example.realtimepolls
.
Choose your minimum SDK. API 19 (Android 4.4) is just fine. Continue with the EmptyActivity
template chosen for you, and finish the wizard.
Let’s stop here for now and set up our Pusher Beams and Channels application.
Creating your Beams and Channels instance
Setting up Pusher Channels
Log in to your Pusher dashboard. If you don’t have an account, create one. Your dashboard should look like this:
Create a new Channels app. You can easily do this by clicking the big Create new Channels app card at the bottom right. When you create a new app, you are provided with keys. Keep them safe as you will soon need them.
Getting your FCM key
Before you can start using Beams, you need an FCM key and a google-services file because Beams relies on Firebase. Go to your Firebase console and create a new project.
When you get to the console, click the Add project card to initialize the app creation wizard. Add the name of your project. Read and accept the terms of conditions. After this, you will be directed to the project overview screen. Choose the Add Firebase to your Android app option. The next screen will require the package name of your app.
An easy way to get the package name of your app is from your AndroidManifest.xml
file. Check the <manifest>
tag and copy the value of the package
attribute. Another place you can find this is your app-module build.gradle
file. Look out for the applicationId
value. When you enter the package name and click Register app.
Next, download your google-services.json
file. After you have downloaded the file, you can skip the rest of the process. Add the downloaded file to the app folder of your app RealtimePolls/app
.
Next, go to your Firebase project settings, under the Cloud messaging tab, copy your server key.
Setting up Pusher Beams
Next, log in to the new Pusher dashboard, in here we will create a Pusher Beams instance. You should sign up if you don’t have an account yet. Click on the Beams button on the sidebar then click Create, this will launch a pop up to Create a new Beams instance and give it a name.
As soon as you create the instance, you will be presented with a quickstart guide. Select the ANDROID quickstart
The next screen requires the FCM key you copied earlier. After you add the FCM key, you can exit the quickstart guide.
Building the Android application
Adding our dependencies
Reopen our project in Android Studio. The next thing we need to do is install the necessary dependencies for our app. Open your app-module build.gradle
file and add these:
// File: ./app/build.gradle
dependencies {
// other dependencies...
implementation 'com.pusher:pusher-java-client:1.5.0'
implementation 'com.google.firebase:firebase-messaging:17.0.0'
implementation 'com.pusher:push-notifications-android:0.10.0'
implementation 'com.pusher:pusher-java-client:1.5.0'
implementation "com.squareup.retrofit2:retrofit:2.4.0"
implementation "com.squareup.retrofit2:converter-scalars:2.4.0"
implementation "com.squareup.retrofit2:converter-gson:2.3.0"
}
apply plugin: 'com.google.gms.google-services'
And in the project build.gradle
file add this:
// File: ./build.gradle
dependencies {
// add other dependencies...
classpath 'com.google.gms:google-services:4.0.0'
}
After adding the dependencies, sync your Gradle files so that the dependencies are imported.
Developing the logic for our Android application
Pusher Beams makes use of a service to notify the app when there is a remote message. Create a new service named NotificationsMessagingService
and paste this:
// File: ./app/src/main/java/com/example/realtimepolls/NotificationsMessagingService.kt
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Intent
import android.os.Build
import android.support.v4.app.NotificationCompat
import android.support.v4.app.NotificationManagerCompat
import com.google.firebase.messaging.RemoteMessage
import com.pusher.pushnotifications.fcm.MessagingService
class NotificationsMessagingService : MessagingService() {
override fun onMessageReceived(remoteMessage: RemoteMessage) {
val notificationId = 10
val channelId = "polls"
lateinit var channel:NotificationChannel
val intent = Intent(this, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
val pendingIntent = PendingIntent.getActivity(this, 0, intent, 0)
val mBuilder = NotificationCompat.Builder(this, channelId)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle(remoteMessage.notification!!.title!!)
.setContentText(remoteMessage.notification!!.body!!)
.setContentIntent(pendingIntent)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setAutoCancel(true)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationManager = applicationContext.getSystemService(NotificationManager::class.java)
val name = getString(R.string.channel_name)
val description = getString(R.string.channel_description)
val importance = NotificationManager.IMPORTANCE_DEFAULT
channel = NotificationChannel("world-cup", name, importance)
channel.description = description
notificationManager!!.createNotificationChannel(channel)
notificationManager.notify(notificationId, mBuilder.build())
} else {
val notificationManager = NotificationManagerCompat.from(this)
notificationManager.notify(notificationId, mBuilder.build())
}
}
}
The method onMessageReceived
is called when a push notification is received on the device. The message received is then used to display a notification for the user.
Add the following to your string.xml
file:
// File: ./app/src/main/res/values/strings.xml
<string name="channel_name">Polls</string>
<string name="channel_description">To receive updates about polls</string>
Add the service to your AndroidManifest.xml
file like so:
// File: ./app/src/main/AndroidManifest.xml
<application
...>
[...]
<service android:name=".NotificationsMessagingService">
<intent-filter android:priority="1">
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
</application>
Create an interface named ApiService
and paste the following:
// File: ./app/src/main/java/com/example/realtimepolls/ApiService.kt
import okhttp3.RequestBody
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
interface ApiService {
@GET("/generate")
fun generatePolls(): Call<String>
@POST("/update")
fun updatePolls(@Body body: RequestBody):Call<String>
}
This interface contains the endpoints to be accessed during the course of this tutorial. There are two endpoints, the first one is to get the question and options from the server while the second is to send the option selected by the user to the server.
Since internet connection is required for some functionalities, you need to request for the internet permissions. Add this to your AndroidManifest.xml
file:
// File: ./app/src/main/AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.realtimepolls">
<uses-permission android:name="android.permission.INTERNET"/>
[...]
</manifest>
Next, let’s design the layout of the app. The app will contain radio buttons so as to ensure that only one option is chosen. Open your activity_main.xml
file and paste this:
// File: ./app/src/main/res/layout/activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<ScrollView 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.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:context=".MainActivity">
<TextView
android:id="@+id/poll_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:textSize="20sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<RadioGroup xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/radio_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:orientation="vertical"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/poll_title">
<RadioButton
android:id="@+id/choice_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<android.support.v4.widget.ContentLoadingProgressBar
android:id="@+id/progress_choice_1"
style="@style/Base.Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="200dp"
android:layout_height="50dp"
android:layout_marginStart="10dp"
android:max="100" />
<RadioButton
android:id="@+id/choice_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<android.support.v4.widget.ContentLoadingProgressBar
android:id="@+id/progress_choice_2"
style="@style/Base.Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="200dp"
android:layout_height="50dp"
android:layout_marginStart="10dp"
android:max="100" />
<RadioButton
android:id="@+id/choice_3"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<android.support.v4.widget.ContentLoadingProgressBar
android:id="@+id/progress_choice_3"
style="@style/Base.Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="200dp"
android:layout_height="50dp"
android:layout_marginStart="10dp"
android:max="100" />
</RadioGroup>
<Button
android:id="@+id/vote"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:text="Vote"
android:textAllCaps="false"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/radio_group" />
</android.support.constraint.ConstraintLayout>
</ScrollView>
The layout contains radio buttons with a progress bar below each of them. The progress bar will give a visual feedback of the vote count.
Go to your MainActivity
file and add this:
// File: ./app/src/main/java/com/example/realtimepolls/MainActivity.kt
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import android.support.v7.app.AppCompatActivity
import com.pusher.client.Pusher
import com.pusher.client.PusherOptions
import com.pusher.pushnotifications.PushNotifications
import kotlinx.android.synthetic.main.activity_main.*
import okhttp3.MediaType
import okhttp3.OkHttpClient
import okhttp3.RequestBody
import org.json.JSONObject
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.scalars.ScalarsConverterFactory
class MainActivity : AppCompatActivity() {
private val apiService: ApiService by lazy {
Retrofit.Builder()
.baseUrl("http://10.0.2.2:5000/")
.addConverterFactory(ScalarsConverterFactory.create())
.client(OkHttpClient.Builder().build())
.build().create(ApiService::class.java)
}
val tag = "MainActivity"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
generatePolls()
setupPusher()
setupBeams()
setupClickListener()
}
}
Above, the class variables apiService
and tag
are declared. The first is to be used to make API calls to the local server while the second will be used for logging. In the onCreate
method, there are some other custom methods called. Let’s create them.
First is the generatePolls
method. Paste the function in your MainActivity
class:
private fun generatePolls() {
apiService.generatePolls().enqueue(object : Callback<String> {
override fun onFailure(call: Call<String>?, t: Throwable?) {
}
override fun onResponse(call: Call<String>?, response: Response<String>?) {
val jsonObject = JSONObject(response!!.body())
poll_title.text = jsonObject.getString("title")
choice_1.text = jsonObject.getString("choice1")
choice_2.text = jsonObject.getString("choice2")
choice_3.text = jsonObject.getString("choice3")
}
})
}
This method makes a network call to the server to get the poll question and options and populate the questions and options to the layout.
Next, is the setupPusher
method. Add the following to the MainActivity
class:
private fun setupPusher() {
val options = PusherOptions()
options.setCluster(PUSHER_APP_CLUSTER)
val pusher = Pusher(PUSHER_API_KEY, options)
val channel = pusher.subscribe("polls")
channel.bind("vote") { channelName, eventName, data ->
Log.d(tag, data)
val jsonObject = JSONObject(data)
runOnUiThread {
progress_choice_1.progress = jsonObject.getInt("1")
progress_choice_2.progress = jsonObject.getInt("2")
progress_choice_3.progress = jsonObject.getInt("3")
}
}
pusher.connect()
}
Replace the
PUSHER_KEY_*
placeholders with the keys from your Pusher Channels dashboard.
This method subscribes to the polls
channel and listens to the vote
event. Here, what is expected from the Pusher event is the score in percent of each option of the poll. The results are then populated to their respective progress-bars on the UI thread.
Next, create the setupBeams
function and add it to the same class:
private fun setupBeams() {
PushNotifications.start(applicationContext, "PUSHER_BEAMS_INSTANCE_ID")
PushNotifications.subscribe("polls-update")
}
This method above initializes Pusher Beams and subscribes to the polls-update
event.
Replace
PUSHER_BEAMS_INSTANCE_ID
with the instance ID from your Beams dashboard.
Finally, create the setupClickListener
and add it to the class:
private fun setupClickListener() {
vote.setOnClickListener {
val checkedButton = radio_group.checkedRadioButtonId
if (checkedButton == -1) {
Toast.makeText(this, "Please select an option", Toast.LENGTH_SHORT).show()
} else {
Log.d(tag, checkedButton.toString())
val selectedId = when (checkedButton) {
R.id.choice_1 -> 1
R.id.choice_2 -> 2
R.id.choice_3 -> 3
else -> -1
}
val jsonObject = JSONObject()
jsonObject.put("option", selectedId)
val body = RequestBody.create(MediaType.parse("application/json"), jsonObject.toString())
apiService.updatePolls(body).enqueue(object : Callback<String> {
override fun onFailure(call: Call<String>?, t: Throwable?) {
Log.d(tag, t?.localizedMessage)
}
override fun onResponse(call: Call<String>?, response: Response<String>?) {
Log.d(tag, response?.body())
}
})
}
}
}
This method above contains the click listener added to the vote button. The user must choose an option for the vote to be recorded. Based on the choice of the user, a unique ID is sent to the server to update the poll and trigger a Pusher event.
That’s all for the Android application. Let’s build a simple Python backend.
Building your backend
Let’s create our project folder, and activate a virtual environment in it. Run the commands below:
$ mkdir pypolls
$ cd pypolls
$ virtualenv .venv
$ source .venv/bin/activate # Linux based systems
$ \path\to\env\Scripts\activate # Windows users
Now that we have the virtual environment setup, we can install Flask within it with this command:
$ pip install flask
Next, run the following command to set the Flask environment to development (on Linux based machines):
$ export FLASK_ENV=development
If you are on Windows, the environment variable syntax depends on command line interpreter. On Command Prompt:
C:\path\to\app>set FLASK_APP=app.py
And on PowerShell:
PS C:\path\to\app> $env:FLASK_APP = "app.py"
Now we need to install some of the other dependencies:
$ pip install pusher pusher_push_notifications
$ pip install --ignore-installed pyopenssl
When the installation is complete, create the main and only Python file called app.py
and paste the following code:
// File: ./app.py
# Imports
from flask import Flask, jsonify, request, json
from pusher import Pusher
from pusher_push_notifications import PushNotifications
app = Flask(__name__)
pn_client = PushNotifications(
instance_id='YOUR_INSTANCE_ID_HERE',
secret_key='YOUR_SECRET_KEY_HERE',
)
pusher = Pusher(app_id=u'PUSHER_APP_ID', key=u'PUSHER_APP_KEY', secret=u'PUSHER_SECRET', cluster=u'PUSHER_CLUSTER')
# Variables to hold scores of polls
choice1 = 0
choice2 = 0
choice3 = 0
# Route to send poll question
@app.route('/generate')
def send_poll_details():
return jsonify({'title':'Who will win the 2018 World Cup','choice1': 'Germany', 'choice2':'Brazil', 'choice3':'Spain'})
@app.route('/update', methods=['POST'])
def update_poll():
global choice1, choice2, choice3
req_data = request.get_json()
user_choice = req_data['option']
if user_choice == 1:
choice1 += 1
elif user_choice == 2:
choice2 += 1
elif user_choice == 3:
choice3 += 1
else:
print("User choose a wrong option")
total = 0.0
total = float(choice1 + choice2 + choice3)
choice1_percent = (choice1/total) * 100
choice2_percent = (choice2/total) * 100
choice3_percent = (choice3/total) * 100
pn_client.publish(
interests=['polls-update'],
publish_body={
'fcm': {
'notification': {
'title': 'Polls update',
'body': 'There are currently ' + str(int(round(total))) + 'vote(s) in the polls. Have you casted your vote?',
},
},
},
)
pusher.trigger(u'polls', u'vote', {u'1': choice1_percent, '2':choice2_percent, '3':choice3_percent})
return 'success', 200
Replace the
PUSHER_APP_*
keys with the credentials from your Pusher dashboard.
This is the only file needed for your Flask application. This snippet contains two endpoints to send out the poll question and to give current results.
Run your Python app using this command:
$ flask run
Now run your Android application in Android Studio and you should see something like this:
Conclusion
In this post, you have learned briefly about Flask and how to use it to develop RESTful APIs. You have also explored Pusher’s realtime technologies both on the client and server side. Feel free to check out the final GitHub repo and play around with the application.
9 July 2018
by Neo Ighodaro