Build

Build a Geo-aware, Location-based Android Dating App

10 min read Syed Ahmed on Feb 14, 2019

With over a third of people opting to create meaningful relationships online, it’s only fitting that instant gratification-driven dating apps like Tinder and Bumble have flourished. That got me thinking – how hard is it to build a geo-aware mobile dating app from scratch? Turns out, with microservices and serverless design patterns, backed by a real-time network, it’s not so hard.

In this tutorial, we’ll cover two very important parts of building an Android GPS dating app.



Shoutout to Dan for making this!

Microservices Architecture for an Android GPS Dating App

Let’s cover the flow of our application and cover a quick overview of what we’ll build. To keep things straightforward, when I say user I’m referring the person who opens the Android app, and when I say partner(s) I’m referring to every other user who opens the application.

We know that we need to find every partner aside from the user, and we also need to know their location. This means that every device needs to share a unique ID and their location. Next, we need each device to be able to check against each other device while also adding themselves to list or updating their current location. Once the user has added themselves to the list of partners, we can choose every other user from the list and check their distance against the current user’s.

That means we can split our whole system up into three parts:

Android Application

The actual Android application that sends it’s own unique ID with location and receives the ID and location of other users.

Save and Filter

This section ingests data from the Android application and returns out the location and unique ID of every user who isn’t the one who called the service.

Calculate Distance

This takes in a user with their location as well as the location of another user and spit back the distance. There is some math involved because we’ll be calculating the distance between two latitude and longitude distances. This service will return the unique user and the distance.

Tinder-Bumble-Swiping-Architecture

Creating Microservices

To make things simple and efficient, we need to find a provider to run our microservices. To do so, we’ll use Functions.

You’ll first have to sign up for an account using the embedded form below. After that, head over to the Admin Dashboard and enable the Functions feature.

This will let us build out the Save and Filter feature, as well as the Calculate Distance microservice on PubNub, and give us the real-time, scalable experience we want.

Saving and Filtering Users in Real time

Our client application will publish the current user’s ID and location to a serverless Function, which will save the location to a keyset-wide persistent storage called PubNub KV Store.

From there, our first Function will check the current ID against every item in the KV Store and append it to the list of users. Once we have the full list, we’ll publish that message back to channel that’s unique to the device using its ID.  

export default (request) => { 
    const kvstore = require('kvstore');
    const xhr = require('xhr');
    const pubnub = require('pubnub');
    const {location, id} = JSON.parse(request.message);
    var people = [];
    
    kvstore.set(id, {lat: location.lat, long: location.long});
    kvstore.getKeys().then((keys) => {
        for(var i=0; i<keys.length;i++){
            if(keys[i] != id){
                people.push(keys[i]);
            }
        }
        pubnub.publish({
            "message": people, 
            "channel": id
        }).then();
    });
    return request.ok();
}

Note: Functions allows of a maximum of 3 requests per function call.

Calculating Distance in Real time

We’ll be getting the data in the form of an array. The first two elements of the array are the IDs of the user and the last two elements are the location of the user who initiated the request. The first element is the ID of the initiator, and the second is a possible swipe candidate. Once we finish the calculation, we’ll send the ID of the unique user and the distance they are from the initiator.

export default (request) => { 
    const kvstore = require('kvstore');
    const xhr = require('xhr');
    const pubnub = require('pubnub');
    
    const message = JSON.parse(request.message);
    
    console.log(message);
    
    kvstore.getItem(message[1]).then((value) => {
    var location = JSON.parse(value);
    
    var distanceDelta = distance(message[2], message[3], location.lat, location.long, "K");
    pubnub.publish({
                "message":{
                    "ID": message[1],
                    "distance": distanceDelta
                }, 
                "channel": `${message[0]}-distance`
            }).then();
    })
        
    
    return request.ok(); // Return a promise when you're done 
   
}
function distance(lat1, lon1, lat2, lon2, unit) {
  if ((lat1 == lat2) && (lon1 == lon2)) {
    return 0;
  }
  else {
      console.log(lat1, lon1, lat2, lon2);
    var radlat1 = Math.PI * lat1/180;
    var radlat2 = Math.PI * lat2/180;
    var theta = lon1-lon2;
    var radtheta = Math.PI * theta/180;
    var dist = Math.sin(radlat1) * Math.sin(radlat2) + Math.cos(radlat1) * Math.cos(radlat2) * Math.cos(radtheta);
    if (dist > 1) {
      dist = 1;
    }
    dist = Math.acos(dist);
    dist = dist * 180/Math.PI;
    dist = dist * 60 * 1.1515;
    if (unit=="K") { dist = dist * 1.609344 }
    if (unit=="N") { dist = dist * 0.8684 }
    return dist;
  }
}

The result of this function will look like this:

{
  "ID": "Unique User ID",
  "distance": 5
}

How to Swipe Through Users on the Android App

Swiping-Microservices-PubNub

To start off, create an empty Android Studio project with Kotlin support checked.

PubNub-Kotlin

Next, look at the dependencies we’re going to add to our app-level Gradle file to ensure our application runs smoothly.

dependencies {
    implementation group: 'com.pubnub', name: 'pubnub-gson', version: '4.20.0'
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
    implementation 'com.google.android.gms:play-services:11.6.0'
    implementation 'com.github.bumptech.glide:glide:4.8.0'
    annotationProcessor 'com.github.bumptech.glide:compiler:4.8.0'
    // Support Library
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support:recyclerview-v7:28.0.0'
    implementation 'com.android.support:cardview-v7:28.0.0'
    implementation 'com.android.support:design:28.0.0'
    // View
    implementation 'com.makeramen:roundedimageview:2.3.0'
    // Card Stack View
    implementation "com.yuyakaido.android:card-stack-view:2.2.1"
}

The first dependency is the PubNub SDK, which will help us publish and subscribe to the logic we just created. Related to the PubNub SDK, we’ll also need our Publish and Subscribe keys. You can get your publish and subscribe keys by going through the quick setup below.

The other dependencies needed are for the visual component of our application – the swiping functionality.

Creating the User Interface

First, we’ll adjust our activity_main.xml to accommodate for our swiping feature that’ll be initialized in our MainActivity.kt file.

<android.support.v4.widget.DrawerLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <android.support.design.widget.AppBarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">
            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="wrap_content">
            </android.support.v7.widget.Toolbar>
        </android.support.design.widget.AppBarLayout>
        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:clipChildren="false">
            <LinearLayout
                android:id="@+id/button_container"
                android:orientation="horizontal"
                android:layout_width="match_parent"
                android:layout_height="80dp"
                android:layout_alignParentBottom="true"
                android:paddingBottom="12dp"
                android:clipChildren="false"
                android:clipToPadding="false">
                <RelativeLayout
                    android:orientation="horizontal"
                    android:layout_width="0dp"
                    android:layout_height="match_parent"
                    android:layout_weight="2"
                    android:paddingRight="16dp"
                    android:paddingEnd="16dp"
                    android:clipToPadding="false">
                    <android.support.design.widget.FloatingActionButton
                        android:id="@+id/skip_button"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_centerVertical="true"
                        android:layout_alignParentRight="true"
                        android:layout_alignParentEnd="true"
                        android:hapticFeedbackEnabled="true"
                        android:src="@drawable/skip_red_24dp"
                        app:backgroundTint="@android:color/white"
                        app:fabSize="auto"
                        app:rippleColor="#22ED7563"/>
                </RelativeLayout>
                <RelativeLayout
                    android:orientation="horizontal"
                    android:layout_width="0dp"
                    android:layout_height="match_parent"
                    android:layout_weight="1">
                    <android.support.design.widget.FloatingActionButton
                        android:id="@+id/rewind_button"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_centerInParent="true"
                        android:hapticFeedbackEnabled="true"
                        android:src="@drawable/rewind_blue_24dp"
                        app:backgroundTint="@android:color/white"
                        app:fabSize="mini"
                        app:rippleColor="#225BC9FA"/>
                </RelativeLayout>
                <RelativeLayout
                    android:orientation="horizontal"
                    android:layout_width="0dp"
                    android:layout_height="match_parent"
                    android:layout_weight="2"
                    android:paddingLeft="16dp"
                    android:paddingStart="16dp"
                    android:clipToPadding="false">
                    <android.support.design.widget.FloatingActionButton
                        android:id="@+id/like_button"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_centerVertical="true"
                        android:layout_alignParentLeft="true"
                        android:layout_alignParentStart="true"
                        android:hapticFeedbackEnabled="true"
                        android:src="@drawable/like_green_24dp"
                        app:backgroundTint="@android:color/white"
                        app:fabSize="auto"
                        app:rippleColor="#226FE2B3"/>
                </RelativeLayout>
            </LinearLayout>
            <RelativeLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_above="@+id/button_container"
                android:padding="20dp"
                android:clipToPadding="false"
                android:clipChildren="false">
                <com.yuyakaido.android.cardstackview.CardStackView
                    android:id="@+id/card_stack_view"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent">
                </com.yuyakaido.android.cardstackview.CardStackView>
            </RelativeLayout>
        </RelativeLayout>
    </LinearLayout>
    <android.support.design.widget.NavigationView
        android:id="@+id/navigation_view"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:fitsSystemWindows="true">
    </android.support.design.widget.NavigationView>
</android.support.v4.widget.DrawerLayout>

Next, we’ll create each profile card’s UI, as well as the overlay on each of them, taking into consideration whether the user is swiping to the left or right.

<?xml version="1.0" encoding="utf-8"?>
<!-- https://qiita.com/ntsk/items/dac92596742e18470a55 -->
<android.support.v7.widget.CardView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="?attr/selectableItemBackground"
    android:foreground="?attr/selectableItemBackground"
    app:cardUseCompatPadding="true"
    app:cardPreventCornerOverlap="false"
    app:cardCornerRadius="8dp"
    app:cardBackgroundColor="@android:color/white">
    <com.makeramen.roundedimageview.RoundedImageView
        android:id="@+id/item_image"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="centerCrop"
        app:riv_corner_radius="8dp"/>
    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:padding="16dp"
        android:background="@drawable/gradation_black">
        <TextView
            android:id="@+id/item_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textStyle="bold"
            android:textColor="@android:color/white"
            android:textSize="26sp"/>
        <TextView
            android:id="@+id/item_city"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textStyle="bold"
            android:textColor="@android:color/white"
            android:textSize="20sp"/>
    </LinearLayout>
    <FrameLayout
        android:id="@+id/left_overlay"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@drawable/overlay_black">
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/skip_white_120dp"
            android:layout_gravity="center"/>
    </FrameLayout>
    <FrameLayout
        android:id="@+id/right_overlay"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@drawable/overlay_black">
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/like_white_120dp"
            android:layout_gravity="center"/>
    </FrameLayout>
    <FrameLayout
        android:id="@+id/top_overlay"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    </FrameLayout>
    <FrameLayout
        android:id="@+id/bottom_overlay"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    </FrameLayout>
</android.support.v7.widget.CardView>

That’s it for the UI, now let’s cover the backend.

Integrating the Application Logic

For our application to be complete we’ll be creating four separate files. The first file we’re going to need is a class that will act as an object for each profile and will contain the related information.

// file: Spot.kt
data class Spot(
        val id: Long = counter++,
        var name: String,
        val distance: String,
        val url: String
) {
    companion object {
        private var counter = 0L
    }
}

Next, we’re going to create a file that will have some helper functions to update our collection of profiles.

// SpotDiffCallback.kt
import android.support.v7.util.DiffUtil
class SpotDiffCallback(
        private val old: List<Spot>,
        private val new: List<Spot>
) : DiffUtil.Callback() {
    override fun getOldListSize(): Int {
        return old.size
    }
    override fun getNewListSize(): Int {
        return new.size
    }
    override fun areItemsTheSame(oldPosition: Int, newPosition: Int): Boolean {
        return old[oldPosition].id == new[newPosition].id
    }
    override fun areContentsTheSame(oldPosition: Int, newPosition: Int): Boolean {
        return old[oldPosition] == new[newPosition]
    }
}

Now, we can load each profile into the frontend. We’ll do this within a class called the CardStackAdapter.

// CardStackAdapter.kt
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import com.bumptech.glide.Glide
class CardStackAdapter(
        private var spots: List<Spot> = emptyList()
) : RecyclerView.Adapter<CardStackAdapter.ViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val inflater = LayoutInflater.from(parent.context)
        return ViewHolder(inflater.inflate(R.layout.item_spot, parent, false))
    }
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val spot = spots[position]
        holder.name.text = "${spot.id}. ${spot.name}"
        holder.city.text = spot.distance + "km"
        Glide.with(holder.image)
                .load(spot.url)
                .into(holder.image)
        holder.itemView.setOnClickListener { v ->
            Toast.makeText(v.context, spot.name, Toast.LENGTH_SHORT).show()
        }
    }
    override fun getItemCount(): Int {
        return spots.size
    }
    fun setSpots(spots: List<Spot>) {
        this.spots = spots
    }
    fun getSpots(): List<Spot> {
        return spots
    }
    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val name: TextView = view.findViewById(R.id.item_name)
        var city: TextView = view.findViewById(R.id.item_city)
        var image: ImageView = view.findViewById(R.id.item_image)
    }
}

Stitching Everything Together

We can head over to the MainActivity.kt file to see how everything fits together.

Let’s have a quick look at the onCreate and onStart methods.

class MainActivity : AppCompatActivity(), CardStackListener {
    private val cardStackView by lazy { findViewById<CardStackView>(R.id.card_stack_view) }
    private val manager by lazy { CardStackLayoutManager(this, this) }
    private val adapter by lazy { CardStackAdapter(createSpots("Welcome to Dating Swipe!", "0")) }
    private val MY_PERMISSIONS_REQUESTACCESS_COARSE_LOCATION = 1
    private lateinit var fusedLocationClient: FusedLocationProviderClient
    private val pnConfiguration = PNConfiguration()
    init {
        pnConfiguration.publishKey = "YOUR-PUB-KEY"
        pnConfiguration.subscribeKey = "YOUR-SUB-KEY"
    }
    private val pubNub = PubNub(pnConfiguration)
    private val userLocation = mutableListOf<Double>(0.0,0.0)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val androidID = Settings.Secure.getString(this.contentResolver, Settings.Secure.ANDROID_ID)
        fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
        if (checkSelfPermission(this, android.Manifest.permission.ACCESS_COARSE_LOCATION)
                != PackageManager.PERMISSION_GRANTED) {
            if (ActivityCompat.shouldShowRequestPermissionRationale(this,
                            android.Manifest.permission.ACCESS_COARSE_LOCATION)) {
            } else {
                ActivityCompat.requestPermissions(this,
                        arrayOf(android.Manifest.permission.ACCESS_COARSE_LOCATION),
                        MY_PERMISSIONS_REQUESTACCESS_COARSE_LOCATION)
            }
        } else {
            fusedLocationClient.lastLocation
                    .addOnSuccessListener { location: Location? ->
                        if (location != null) {
                            userLocation[0] = location.latitude
                            userLocation[1] = location.longitude
                            Log.d("Location", location.latitude.toString())
                        } else {
                            Log.d("Location", location?.latitude.toString())
                        }
                    }
        }
        var subscribeCallback: SubscribeCallback = object : SubscribeCallback() {
            override fun status(pubnub: PubNub, status: PNStatus) {
            }
            override fun message(pubnub: PubNub, message: PNMessageResult) {
                Log.d("PubNub", message.message.toString())
                var person = message.message.asJsonObject
                runOnUiThread { paginate(person.get("ID").toString(), person.get("distance").toString()) }
            }
            override fun presence(pubnub: PubNub, presence: PNPresenceEventResult) {
            }
        }
        pubNub.run {
            addListener(subscribeCallback)
            subscribe()
                    .channels(Arrays.asList(androidID))
                    .execute()
        }
        setupCardStackView()
        setupButton()
    }
    override fun onStart() {
        super.onStart()
        val androidID = Settings.Secure.getString(this.getContentResolver(), Settings.Secure.ANDROID_ID)
        pubNub.run {
            publish()
                    .message("""
                        {
                            "location": {
                                "lat":${userLocation[0]},
                                "long":${userLocation[1]}
                            },
                            "id": "$androidID"
                        }
                    """.trimIndent())
                    .channel("Users")
                    .async(object : PNCallback<PNPublishResult>() {
                        override fun onResponse(result: PNPublishResult, status: PNStatus) {
                            if (!status.isError) {
                                println("Message was published")
                            } else {
                                println("Could not publish")
                            }
                        }
                    })
        }
    }
}

We can break down everything that’s happening into three things.

First, we’ll get the location of the device using Fused Location. Next, we’ll subscribe to a channel with the same name as our device ID, since all the possible people we can swipe on are published to that channel. Lastly, in the onStart, we’ll be publishing the date related to the device, just like the ID and Location. The reason we publish in the onStart and not the onCreate is because we won’t be able to get all the information we need to publish until the activity starts.

With that, let’s add all the features and using your pub/sub keys (they’re in your Admin Dashboard), in our MainActivity. In the end, our file will look like this:

class MainActivity : AppCompatActivity(), CardStackListener {
    private val cardStackView by lazy { findViewById<CardStackView>(R.id.card_stack_view) }
    private val manager by lazy { CardStackLayoutManager(this, this) }
    private val adapter by lazy { CardStackAdapter(createSpots("Welcome to Dating Swipe!", "0")) }
    private val MY_PERMISSIONS_REQUESTACCESS_COARSE_LOCATION = 1
    private lateinit var fusedLocationClient: FusedLocationProviderClient
    private val pnConfiguration = PNConfiguration()
    init {
        pnConfiguration.publishKey = "YOUR-PUB-KEY"
        pnConfiguration.subscribeKey = "YOUR-SUB-KEY"
    }
    private val pubNub = PubNub(pnConfiguration)
    private val userLocation = mutableListOf<Double>(0.0,0.0)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val androidID = Settings.Secure.getString(this.contentResolver, Settings.Secure.ANDROID_ID)
        fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
        if (checkSelfPermission(this, android.Manifest.permission.ACCESS_COARSE_LOCATION)
                != PackageManager.PERMISSION_GRANTED) {
            if (ActivityCompat.shouldShowRequestPermissionRationale(this,
                            android.Manifest.permission.ACCESS_COARSE_LOCATION)) {
            } else {
                ActivityCompat.requestPermissions(this,
                        arrayOf(android.Manifest.permission.ACCESS_COARSE_LOCATION),
                        MY_PERMISSIONS_REQUESTACCESS_COARSE_LOCATION)
            }
        } else {
            fusedLocationClient.lastLocation
                    .addOnSuccessListener { location: Location? ->
                        if (location != null) {
                            userLocation[0] = location.latitude
                            userLocation[1] = location.longitude
                            Log.d("Location", location.latitude.toString())
                        } else {
                            Log.d("Location", location?.latitude.toString())
                        }
                    }
        }
        var subscribeCallback: SubscribeCallback = object : SubscribeCallback() {
            override fun status(pubnub: PubNub, status: PNStatus) {
            }
            override fun message(pubnub: PubNub, message: PNMessageResult) {
                if(message.message.isJsonArray){
                    for (person: JsonElement in message.message.asJsonArray) {
                        pubNub.run {
                            publish()
                                    .message("""["$androidID", $person, ${userLocation[0]}, ${userLocation[1]}]""")
                                    .channel("distance")
                                    .async(object : PNCallback<PNPublishResult>() {
                                        override fun onResponse(result: PNPublishResult, status: PNStatus) {
                                            if (!status.isError) {
                                                println("Message was published")
                                            } else {
                                                println("Could not publish")
                                            }
                                        }
                                    })
                        }
                    }
                }else{
                    var person = message.message.asJsonObject
                    runOnUiThread { paginate(person.get("ID").toString(), person.get("distance").toString()) }
                }
            }
            override fun presence(pubnub: PubNub, presence: PNPresenceEventResult) {
            }
        }
        pubNub.run {
            addListener(subscribeCallback)
            subscribe()
                    .channels(Arrays.asList(androidID, "$androidID-distance"))
                    .execute()
        }
        setupCardStackView()
        setupButton()
    }
    override fun onStart() {
        super.onStart()
        val androidID = Settings.Secure.getString(this.getContentResolver(), Settings.Secure.ANDROID_ID)
        pubNub.run {
            publish()
                    .message("""
                        {
                            "location": {
                                "lat":${userLocation[0]},
                                "long":${userLocation[1]}
                            },
                            "id": "$androidID"
                        }
                    """.trimIndent())
                    .channel("Users")
                    .async(object : PNCallback<PNPublishResult>() {
                        override fun onResponse(result: PNPublishResult, status: PNStatus) {
                            if (!status.isError) {
                                println("Message was published")
                            } else {
                                println("Could not publish")
                            }
                        }
                    })
        }
    }
    override fun onCardDragging(direction: Direction, ratio: Float) {
        Log.d("CardStackView", "onCardDragging: d = ${direction.name}, r = $ratio")
    }
    override fun onCardSwiped(direction: Direction) {
        Log.d("CardStackView", "onCardSwiped: p = ${manager.topPosition}, d = $direction")
        if (manager.topPosition == adapter.itemCount - 5) {
            paginate("", "")
        }
    }
    override fun onCardRewound() {
        Log.d("CardStackView", "onCardRewound: ${manager.topPosition}")
    }
    override fun onCardCanceled() {
        Log.d("CardStackView", "onCardCanceled: ${manager.topPosition}")
    }
    override fun onCardAppeared(view: View, position: Int) {
        val textView = view.findViewById<TextView>(R.id.item_name)
        Log.d("CardStackView", "onCardAppeared: ($position) ${textView.text}")
    }
    override fun onCardDisappeared(view: View, position: Int) {
        val textView = view.findViewById<TextView>(R.id.item_name)
        Log.d("CardStackView", "onCardDisappeared: ($position) ${textView.text}")
    }
    private fun setupCardStackView() {
        initialize()
    }
    private fun setupButton() {
        val skip = findViewById<View>(R.id.skip_button)
        skip.setOnClickListener {
            val setting = SwipeAnimationSetting.Builder()
                    .setDirection(Direction.Left)
                    .setDuration(200)
                    .setInterpolator(AccelerateInterpolator())
                    .build()
            manager.setSwipeAnimationSetting(setting)
            cardStackView.swipe()
        }
        val rewind = findViewById<View>(R.id.rewind_button)
        rewind.setOnClickListener {
            val setting = RewindAnimationSetting.Builder()
                    .setDirection(Direction.Bottom)
                    .setDuration(200)
                    .setInterpolator(DecelerateInterpolator())
                    .build()
            manager.setRewindAnimationSetting(setting)
            cardStackView.rewind()
        }
        val like = findViewById<View>(R.id.like_button)
        like.setOnClickListener {
            val setting = SwipeAnimationSetting.Builder()
                    .setDirection(Direction.Right)
                    .setDuration(200)
                    .setInterpolator(AccelerateInterpolator())
                    .build()
            manager.setSwipeAnimationSetting(setting)
            cardStackView.swipe()
        }
    }
    private fun initialize() {
        manager.setStackFrom(StackFrom.None)
        manager.setVisibleCount(3)
        manager.setTranslationInterval(8.0f)
        manager.setScaleInterval(0.95f)
        manager.setSwipeThreshold(0.3f)
        manager.setMaxDegree(20.0f)
        manager.setDirections(Direction.HORIZONTAL)
        manager.setCanScrollHorizontal(true)
        manager.setCanScrollVertical(true)
        cardStackView.layoutManager = manager
        cardStackView.adapter = adapter
        cardStackView.itemAnimator.apply {
            if (this is DefaultItemAnimator) {
                supportsChangeAnimations = false
            }
        }
    }
    private fun paginate(name: String?, distance: String?) {
        val old = adapter.getSpots()
        val new = old.plus(createSpots("Person: $name", "Distance: $distance"))
        val callback = SpotDiffCallback(old, new)
        val result = DiffUtil.calculateDiff(callback)
        adapter.setSpots(new)
        result.dispatchUpdatesTo(adapter)
    }
    private fun createSpots(personName: String, personDistance: String): List<Spot> {
        val spots = ArrayList<Spot>()
        spots.add(Spot(
                name = personName,
                distance = personDistance,
                url = "https://picsum.photos/200/300/?random"
        ))
        return spots
    }
}

Let’s run the app! In either an emulator or on a device, you can see the swiping functionality, as well as the user’s distance from you.

Swiping-Microservices-PubNub

Nice work! Want to explore more features and ideas around mobile dating apps? Check out our real-time dating apps overview, and see how you can power cross-platform, fast, and secure dating apps at global scale with PubNub’s chat APIs and messaging infrastructure.

0