Developer Relations, PubNub
IN THIS ARTICLE

    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 realtime network, it’s not so hard.

    In this tutorial, we’ll cover two very important parts of building a mobile, geo-aware dating application – geolocation and swiping.



    Shoutout to Dan for making this!

    Microservices Architecture for a 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.

    Creating Microservices

    To make things simple and efficient, we need to find a provider to run our microservices. To do so, we’ll use PubNub 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 realtime, scalable experience we want.

    Saving and Filtering Users in Realtime

    Our client application will publish the current user’s ID and location to a serverless PubNub 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: PubNub Functions allows of a maximum of 3 requests per function call.

    Calculating Distance in Realtime

    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

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

    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.

    Nice work! Want to explore more features and ideas around mobile dating apps? Check out our realtime 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.

    Try PubNub today!

    Build realtime applications that perform reliably and securely, at global scale.
    Try Our APIs
    Try PubNub today!
    More From PubNub