by Neo Ighodaro

由新Ighodaro

When building a chat application, it is essential to have an online presence feature. It is essential because your users will like to know when their friends are online, and are more likely to respond to their messages in real time.

构建聊天应用程序时,必须具有在线状态功能。 这很重要,因为您的用户希望知道他们的朋友何时在线,并且更有可能实时响应他们的消息。

In this article, we will be building a messenger app with online presence using Pusher Channels, Kotlin, and Node.js.

在本文中,我们将使用Pusher Channels,Kotlin和Node.js构建具有在线状态的Messenger应用程序。

Here is a demo of what we will build:

这是我们将构建的演示:

先决条件 (Prerequisites)

To follow along you need the following requirements:

要遵循,您需要满足以下要求:

  • A Pusher Channel app. You can create one here.

    Pusher Channel应用。 您可以在此处创建一个。

  • Android Studio installed on your machine. You can check here for the latest stable version. A minimum of version 3.0 is recommended.

    您的计算机上安装了Android Studio。 您可以在此处查看最新的稳定版本。 建议最低版本为3.0。

  • Basic knowledge of Android development and the Android Studio IDE.Android开发和Android Studio IDE的基础知识。
  • Basic knowledge of Kotlin. Here are the official docs.

    Kotlin的基础知识。 这是官方文档 。

  • Node.js and NPM (Node Package Manager) installed on your machine. Download here.

    您的计算机上安装了Node.js和NPM(节点程序包管理器)。 在这里下载。

  • Mongo DB installed on your machine. You can install it following the instructions here.

    您的计算机上安装了Mongo DB。 您可以按照此处的说明进行安装。

Some familiarity with Android development is also required.

还需要对Android开发有所了解。

构建后端服务器 (Building the backend server)

Our server will be built using Node.js. To start, create a new project directory:

我们的服务器将使用Node.js构建。 首先,创建一个新的项目目录:

$ mkdir backend-server

Next, create a new index.js file inside the project directory and paste the following code:

接下来,在项目目录中创建一个新的index.js文件,并粘贴以下代码:

// File: ./index.js    var express = require('express');    var bodyParser = require('body-parser');    const mongoose = require('mongoose');    var Pusher = require('pusher');
var app = express();
app.use(bodyParser.json());    app.use(bodyParser.urlencoded({ extended: false }));
var pusher = new Pusher({      appId: 'PUSHER_APP_ID',      key: 'PUSHER_APP_KEY',      secret: 'PUSHER_APP_SECRET',      cluster: 'PUSHER_APP_CLUSTER'    });
mongoose.connect('mongodb://127.0.0.1/db');
const Schema = mongoose.Schema;    const userSchema = new Schema({        name: { type: String, required: true, },        count: {type: Number}    });
var User = mongoose.model('User', userSchema);    userSchema.pre('save', function(next) {        if (this.isNew) {            User.count().then(res => {              this.count = res; // Increment count              next();            });          } else {            next();          }    });
module.exports = User;
var currentUser;
/*     =================================    We will add our endpoints here!!!    =================================    */
var port = process.env.PORT || 5000;
app.listen(port);

In the snippet above, we initialized Pusher, Express, and MongoDB. We are using Moongose to connect to our MongoDB instance.

在上面的代码段中,我们初始化了Pusher,Express和MongoDB。 我们正在使用Moongose连接到我们的MongoDB实例。

Replace the PUSHER_APP_* keys with the ones on your Pusher dashboard.

PUSHER_APP_*键替换为Pusher仪表板上的键。

Now let’s add our endpoints. The first endpoint we will add will be to log a user in. Paste the code below in your index.js file below the currentUser declaration:

现在让我们添加端点。 我们将添加的第一个端点将是登录用户。将下面的代码粘贴到index.js文件中的currentUser声明下方:

// File: ./index.js
// [...]
app.post('/login', (req,res) => {        User.findOne({name: req.body.name}, (err, user) => {            if (err) {                res.send("Error connecting to database");            }
// User exists            if (user) {                currentUser = user;                return res.status(200).send(user)            }
let newuser = new User({name: req.body.name});
newuser.save(function(err) {                if (err) throw err;            });
currentUser = newuser;            res.status(200).send(newuser)        });    })
// [...]

This endpoint receives a username with the request, and either creates a new user or returns the data of the existing user.

该端点接收带有请求的username ,并创建一个新用户或返回现有用户的数据。

Let’s add the next endpoint below the one above:

让我们在上面的端点下面添加下一个端点:

// File: ./index.js
// [...]
app.get('/users', (req,res) => {        User.find({}, (err, users) => {            if (err) throw err;            res.send(users);        });    })
// [...]

This second endpoint fetches all the users from the database and returns them.

第二个端点从数据库中获取所有用户并返回它们。

Since we will be using a Pusher presence channel, we need an endpoint to authenticate the user. In the same file, paste this code below the endpoint above:

由于我们将使用Pusher状态通道,因此我们需要一个端点来验证用户身份。 在同一文件中,将此代码粘贴到上方端点以下:

// File: ./index.js
// [...]
app.post('/pusher/auth/presence', (req, res) => {        let socketId = req.body.socket_id;        let channel = req.body.channel_name;
let presenceData = {            user_id: currentUser._id,            user_info: {count: currentUser.count, name: currentUser.name}        };
let auth = pusher.authenticate(socketId, channel, presenceData);
res.send(auth);    });
// [...]

Since we are going to be using private channels, we need an endpoint for authentication. Add the following endpoint below the endpoint above:

由于我们将使用专用通道,因此我们需要一个端点进行身份验证。 在上方端点下方添加以下端点:

// File: ./index.js
// [...]
app.post('/pusher/auth/private', (req, res) => {        res.send(pusher.authenticate(req.body.socket_id, req.body.channel_name));    });
// [...]
Finally, the last endpoint will be to trigger an event `new-message` to a channel. Add the endpoint below the last one:
// File: ./index.js
// [...]
app.post('/send-message', (req, res) => {        let payload = {message: req.body.message, sender_id: req.body.sender_id}        pusher.trigger(req.body.channel_name, 'new-message', payload);        res.send(200);    });
// [...]

After adding all the endpoints, install the necessary npm packages by running this command:

添加所有端点之后,通过运行以下命令来安装必要的npm软件包:

$ npm install express body-parser mongoose pusher

Before you run your application, make sure MongoDB is running already using this command:

在运行应用程序之前,请使用以下命令确保MongoDB已经在运行:

$ mongod --dbpath C:\MongoDB\data\db # Windows    $ mongod --dbpath=/path/to/db/directory # Mac or Linux

Now you can run your application using the command below:

现在,您可以使用以下命令运行您的应用程序:

$ node index.js

Your app will be available here: http://localhost:5000.

您的应用程序将在此处提供: http:// localhost:5000 。

构建我们的Android应用程序 (Building our Android application)

Create your Android project. In the wizard, enter your project name — let’s say MessengerApp.

创建您的Android项目。 在向导中,输入您的项目名称-假设为MessengerApp。

Next, enter your package name. You can use a minimum SDK of 19 then choose an Empty Activity.

接下来,输入您的包裹名称。 您可以使用最低19的SDK,然后选择空活动

On the next page, change the Activity Name to LoginActivity. After this, Android Studio will build your project for you.

在下一页上,将“ 活动名称”更改为LoginActivity 。 之后,Android Studio将为您构建项目。

Now that we have the project, let’s add the required dependencies for our app. Open your app module build.gradle file and add these:

现在我们有了项目,让我们为应用程序添加必需的依赖项。 打开您的应用程序模块build.gradle文件并添加以下内容:

// File ../app/build.gradle    dependencies {      // [...]
implementation 'com.android.support:design:28+'      implementation 'com.pusher:pusher-java-client:1.6.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'    }

Notably, we added the dependencies for Retrofit and Pusher. Retrofit is an HTTP client library used for network calls. We added the design library dependency too as we want to use some classes from it. Sync your gradle files to pull in the dependencies.

值得注意的是,我们添加了Retrofit和Pusher的依赖项。 Retrofit是用于网络调用的HTTP客户端库。 我们还添加了设计库依赖项,因为我们想使用其中的一些类。 同步gradle文件以获取依赖项。

Next, let’s prepare our app to make network calls. Retrofit requires an interface to know the endpoints to be accessed.

接下来,让我们准备应用程序以进行网络通话。 改造需要一个接口来知道要访问的端点。

Create a new interface named ApiService and paste this:

创建一个名为ApiService的新接口,并将其粘贴:

// File: ./app/src/main/java/com/example/messengerapp/ApiService.kt    import okhttp3.RequestBody    import retrofit2.Call    import retrofit2.http.Body    import retrofit2.http.GET    import retrofit2.http.POST
interface ApiService {
@POST("/login")      fun login(@Body body:RequestBody): Call<UserModel>
@POST("/send-message")      fun sendMessage(@Body body:RequestBody): Call<String>
@GET("/users")      fun getUsers(): Call<List<UserModel&gt;>    }

Here, we have declared three endpoints. They are for logging in, sending messages, and fetching users.

在这里,我们声明了三个端点。 它们用于登录,发送消息和获取用户。

In some of our responses, we return Call<UserModel>. Let’s create the UserModel. Create a new class called UserModel and paste the following:

在某些响应中,我们返回Call<UserMod el>。 让我们创建e the Use rModel。 创建一个新的类并alled Use rModel并粘贴以下内容:

// File: ./app/src/main/java/com/example/messengerapp/UserModel.kt    import com.google.gson.annotations.Expose    import com.google.gson.annotations.SerializedName
data class UserModel(@SerializedName("_id") @Expose var id: String,                         @SerializedName("name") @Expose var name: String,                         @SerializedName("count") @Expose var count: Int,                         var online:Boolean = false)

Above, we used a data class so that some other functions required for model classes such as toString and hashCode are added to the class by default.

上面,我们使用了一个数据类,以便默认情况下将模型类所需的其他一些功能(例如toStringhashCode添加到该类中。

We are expecting only the values for the id and name from the server. We added the online property so we can update later on.

我们期望服务器仅提供idname的值。 我们添加了online媒体资源,以便稍后进行更新。

Next, create a new class named RetrofitInstance and paste in the following code:

接下来,创建一个名为RetrofitInstance的新类,并粘贴以下代码:

// File: ./app/src/main/java/com/example/messengerapp/RetrofitInstance.kt    import okhttp3.OkHttpClient    import retrofit2.Retrofit    import retrofit2.converter.gson.GsonConverterFactory    import retrofit2.converter.scalars.ScalarsConverterFactory
class RetrofitInstance {
companion object {        val retrofit: ApiService by lazy {          val httpClient = OkHttpClient.Builder()          val builder = Retrofit.Builder()              .baseUrl("http://10.0.2.2:5000/")              .addConverterFactory(ScalarsConverterFactory.create())              .addConverterFactory(GsonConverterFactory.create())
val retrofit = builder              .client(httpClient.build())              .build()          retrofit.create(ApiService::class.java)        }      }    }

RetrofitInstance contains a class variable called retrofit. It provides us with an instance for Retrofit that we will reference in more than one class.

RetrofitInstance含有一种称为类变量retrofit 。 它为我们提供了Retrofit的实例,我们将在多个类中进行引用。

Finally, to request for the internet access permission update the AndroidManifest.xml file like so:

最后,要请求互联网访问权限,请更新AndroidManifest.xml文件,如下所示:

// File: ./app/src/main/ApiService.kt    <manifest xmlns:android="http://schemas.android.com/apk/res/android"      package="com.example.messengerapp">
<uses-permission android:name="android.permission.INTERNET" />      [...]
</manifest>

Now we can make requests using Retrofit.

现在我们可以使用Retrofit发出请求。

The next feature we will implement is login. Open the already created LoginActivity layout file activity_login.xml file and paste this:

我们将实现的下一个功能是登录。 打开已经创建的LoginActivity布局文件activity_login.xml文件,并将其粘贴:

// File: ./app/src/main/res/layout/activity_login.xml    &lt;?xml version="1.0" encoding="utf-8"?>    <android.support.constraint.ConstraintLayout 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:layout_margin="20dp"      tools:context=".LoginActivity">
<EditText        android:id="@+id/editTextUsername"        android:layout_width="match_parent"        android:layout_height="wrap_content"        app:layout_constraintBottom_toBottomOf="parent"        app:layout_constraintLeft_toLeftOf="parent"        app:layout_constraintRight_toRightOf="parent"        app:layout_constraintTop_toTopOf="parent" />
<Button        android:id="@+id/loginButton"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:text="Login"        app:layout_constraintTop_toBottomOf="@+id/editTextUsername" />
</android.support.constraint.ConstraintLayout>

This layout contains an input field to take the username, and a button to make a login request.

此布局包含一个使用用户名的输入字段,以及一个进行登录请求的按钮。

Next, open the LoginActivity.Kt file and paste in this:

接下来,打开LoginActivity.Kt文件并将其粘贴在其中:

// File: ./app/src/main/java/com/example/messengerapp/LoginActivity.kt    import android.content.Intent    import android.os.Bundle    import android.support.v7.app.AppCompatActivity    import android.util.Log    import kotlinx.android.synthetic.main.activity_login.*    import okhttp3.MediaType    import okhttp3.RequestBody    import org.json.JSONObject    import retrofit2.Call    import retrofit2.Callback    import retrofit2.Response
class LoginActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        setContentView(R.layout.activity_login)        loginButton.setOnClickListener {          if (editTextUsername.text.isNotEmpty()) {            loginFunction(editTextUsername.text.toString())          }        }      }
private fun loginFunction(name:String) {        val jsonObject = JSONObject()        jsonObject.put("name", name)
val jsonBody = RequestBody.create(            MediaType.parse("application/json; charset=utf-8"),             jsonObject.toString()        )
RetrofitInstance.retrofit.login(jsonBody).enqueue(object:Callback<UserModel> {          override fun onFailure(call: Call<UserModel>?, t: Throwable?) {            Log.i("LoginActivity",t!!.localizedMessage)          }
override fun onResponse(call: Call<UserModel>?, response: Response<UserModel>?) {            if (response!!.code() == 200) {              Singleton.getInstance().currentUser = response.body()!!              startActivity(Intent(this@LoginActivity,ContactListActivity::class.java))              finish()            }          }        })      }    }

In the LoginActivity.Kt file, we set up a listener for our login button so that, when it is clicked, we can send the text to the server for authentication. We also stored the logged in user in a singleton class so that we can access the user’s details later.

LoginActivity.Kt文件中,我们为登录按钮设置了一个侦听器,以便单击该按钮时,可以将文本发送到服务器进行身份验证。 我们还将登录用户存储在单例类中,以便以后可以访问该用户的详细信息。

Create a new class called Singleton and paste in this:

创建一个名为Singleton的新类,并粘贴以下内容:

// File: ./app/src/main/java/com/example/messengerapp/RetrofitInstance.kt    class Singleton {      companion object {        private val ourInstance = Singleton()        fun getInstance(): Singleton {          return ourInstance        }      }      lateinit var currentUser: UserModel    }

Singletongives us access to the currentUser, which is the logged in user.

Singleton使我们可以访问currentUser ,即登录用户。

Next, let’s create a new activity named ContactListActivity. For now, leave the class empty and open the corresponding layout file named activity_contact_list , and paste in the following:

接下来,让我们创建一个名为ContactListActivity的新活动。 现在,将类留空,然后打开名为activity_contact_list的相应布局文件,然后粘贴以下内容:

// File: ./app/src/main/res/layout/activity_contact_list.xml    &lt;?xml version="1.0" encoding="utf-8"?>    <android.support.constraint.ConstraintLayout 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"      tools:context=".ContactListActivity">
<android.support.v7.widget.RecyclerView        android:layout_width="match_parent"        android:id="@+id/recyclerViewUserList"        android:layout_height="match_parent"/>
</android.support.constraint.ConstraintLayout>

The layout contains a recycler view, which will give us all the lists of our contacts fetched from the database. Since we are displaying items in a list, we will need an adapter class to manage how items are inflated to the layout.

该布局包含一个回收站视图,该视图将为我们提供从数据库中获取的所有联系人列表。 由于我们在列表中显示项目,因此我们将需要一个适配器类来管理项目如何放大到布局。

Create a new class named ContactRecyclerAdapter and paste in this:

创建一个名为ContactRecyclerAdapter的新类,并粘贴以下内容:

// File: ./app/src/main/java/com/example/messengerapp/ContactRecyclerAdapter.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 java.util.*
class ContactRecyclerAdapter(private var list: ArrayList<UserModel>, private var listener: UserClickListener)      : RecyclerView.Adapter<ContactRecyclerAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {        return ViewHolder(LayoutInflater.from(parent.context)            .inflate(R.layout.user_list_row, parent, false))      }
override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(list[position])
override fun getItemCount(): Int = list.size
fun showUserOnline(updatedUser: UserModel) {        list.forEachIndexed { index, element ->          if (updatedUser.id == element.id) {            updatedUser.online = true            list[index] = updatedUser            notifyItemChanged(index)          }
}      }
fun showUserOffline(updatedUser: UserModel) {        list.forEachIndexed { index, element ->          if (updatedUser.id == element.id) {            updatedUser.online = false            list[index] = updatedUser            notifyItemChanged(index)          }        }      }
fun add(user: UserModel) {        list.add(user)        notifyDataSetChanged()      }
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {        private val nameTextView: TextView = itemView.findViewById(R.id.usernameTextView)        private val presenceImageView: ImageView = itemView.findViewById(R.id.presenceImageView)
fun bind(currentValue: UserModel) = with(itemView) {          this.setOnClickListener {            listener.onUserClicked(currentValue)          }          nameTextView.text = currentValue.name          if (currentValue.online){            presenceImageView.setImageDrawable(this.context.resources.getDrawable(R.drawable.presence_icon_online))          } else {            presenceImageView.setImageDrawable(this.context.resources.getDrawable(R.drawable.presence_icon))
}
}      }
interface UserClickListener {        fun onUserClicked(user: UserModel)      }    }

This adapter has some overridden methods and some custom methods.

此适配器具有一些替代方法和一些自定义方法。

The onCreateViewHolder inflates how each row will look like. onBindViewHolder binds the data to each item by calling the bind method in the inner ViewHolder class. The getItemCount gives the size of the list.

onCreateViewHolder夸大每一行的外观。 onBindViewHolder通过调用内部ViewHolder类中的bind方法将数据绑定到每个项目。 getItemCount给出列表的大小。

For our custom methods, showUserOffline updates the user and shows when they are offline. While showUserOnline does the opposite. Finally, we have the add method, which adds a new contact to the list and refreshes it.

对于我们的自定义方法, showUserOffline更新用户并显示他们何时离线。 而showUserOnline则相反。 最后,我们有add方法,它将新联系人添加到列表中并刷新它。

In the adapter class above, we used a new layout named user_list_row. Create a new layout user_list_rowand paste this:

在上面的适配器类中,我们使用了一个名为user_list_row的新布局。 创建一个新的布局user_list_row并将其粘贴:

// File: ./app/src/main/res/layout/user_list_row.xml    &lt;?xml version="1.0" encoding="utf-8"?>    <LinearLayout      android:orientation="horizontal"      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="wrap_content"      android:layout_margin="20dp"      android:gravity="center"      tools:context=".LoginActivity">
<ImageView        android:id="@+id/presenceImageView"        android:layout_width="15dp"        android:layout_height="15dp"        app:srcCompat="@drawable/presence_icon" />
<TextView        android:layout_width="match_parent"        android:layout_height="wrap_content"        tools:text="Neo"        android:textSize="20sp"        android:layout_marginStart="10dp"        android:id="@+id/usernameTextView"        app:layout_constraintTop_toBottomOf="@+id/editTextUsername"        />
</LinearLayout>

This layout is the visual representation of how each item on the layout will look like. The layout has an image view that shows the users online status. The layout also has a textview that shows the name of the contact beside the icon. The icons are vector drawables. Let’s create the files.

此布局是布局上每个项目的外观的直观表示。 布局具有一个图像视图,显示用户的在线状态。 该布局还具有一个文本视图,该视图在图标旁边显示联系人的姓名。 图标是矢量可绘制对象。 让我们创建文件。

Create a new drawable named presence_icon_online and paste in this:

创建一个新的名为绘制presence_icon_online和粘贴代码:

// File: ./app/src/main/res/drawable/presence_icon_online.xml    <vector android:height="24dp" android:tint="#3FFC3C"        android:viewportHeight="24.0" android:viewportWidth="24.0"        android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">        <path android:fillColor="#FF000000" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2z"/>    </vector>

Create another drawable named presence_icon and paste in this:

创建另一个名为绘制presence_icon和粘贴代码:

// File: ./app/src/main/res/drawable/presence_icon.xml    <vector android:height="24dp" android:tint="#C0C0C6"        android:viewportHeight="24.0" android:viewportWidth="24.0"        android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">        <path android:fillColor="#FF000000" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2z"/>    </vector>

Next, open the ContactListActivity class and paste in this:

接下来,打开ContactListActivity类并粘贴:

// File: ./app/src/main/java/com/example/messengerapp/ContactListActivity.kt    import android.content.Intent    import android.os.Bundle    import android.support.v7.app.AppCompatActivity    import android.support.v7.widget.LinearLayoutManager    import android.util.Log    import com.pusher.client.Pusher    import com.pusher.client.PusherOptions    import com.pusher.client.channel.PresenceChannelEventListener    import com.pusher.client.channel.User    import com.pusher.client.util.HttpAuthorizer    import kotlinx.android.synthetic.main.activity_contact_list.*    import retrofit2.Call    import retrofit2.Callback    import retrofit2.Response
class ContactListActivity : AppCompatActivity(),        ContactRecyclerAdapter.UserClickListener {
private val mAdapter = ContactRecyclerAdapter(ArrayList(), this)
override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        setContentView(R.layout.activity_contact_list)        setupRecyclerView()        fetchUsers()        subscribeToChannel()      }
}

In ContactListActivity, we initialized the ContactRecyclerAdapter, then called three functions in the onCreate method. Let’s create these new functions.

ContactListActivity ,我们初始化了ContactRecyclerAdapter ,然后在onCreate方法中调用了三个函数。 让我们创建这些新功能。

In the same class, add the following methods:

在同一类中,添加以下方法:

private fun setupRecyclerView() {      with(recyclerViewUserList) {        layoutManager = LinearLayoutManager(this@ContactListActivity)        adapter = mAdapter      }    }
private fun fetchUsers() {      RetrofitInstance.retrofit.getUsers().enqueue(object : Callback<List<UserModel>> {        override fun onFailure(call: Call<List<UserModel>>?, t: Throwable?) {}        override fun onResponse(call: Call<List<UserModel>>?, response: Response<List<UserModel>>?) {          for (user in response!!.body()!!) {            if (user.id != Singleton.getInstance().currentUser.id) {              mAdapter.add(user)            }          }        }      })    }
private fun subscribeToChannel() {
val authorizer = HttpAuthorizer("http://10.0.2.2:5000/pusher/auth/presence")      val options = PusherOptions().setAuthorizer(authorizer)      options.setCluster("PUSHER_APP_CLUSTER")
val pusher = Pusher("PUSHER_APP_KEY", options)      pusher.connect()
pusher.subscribePresence("presence-channel", object : PresenceChannelEventListener {        override fun onUsersInformationReceived(p0: String?, users: MutableSet<User>?) {          for (user in users!!) {            if (user.id!=Singleton.getInstance().currentUser.id){              runOnUiThread {                mAdapter.showUserOnline(user.toUserModel())              }            }          }        }
override fun onEvent(p0: String?, p1: String?, p2: String?) {}        override fun onAuthenticationFailure(p0: String?, p1: Exception?) {}        override fun onSubscriptionSucceeded(p0: String?) {}
override fun userSubscribed(channelName: String, user: User) {          runOnUiThread {            mAdapter.showUserOnline(user.toUserModel())          }        }
override fun userUnsubscribed(channelName: String, user: User) {          runOnUiThread {            mAdapter.showUserOffline(user.toUserModel())          }        }      })    }
override fun onUserClicked(user: UserModel) {      val intent = Intent(this, ChatRoom::class.java)      intent.putExtra(ChatRoom.EXTRA_ID,user.id)      intent.putExtra(ChatRoom.EXTRA_NAME,user.name)      intent.putExtra(ChatRoom.EXTRA_COUNT,user.count)      startActivity(intent)    }

Replace the PUSHER_APP_* keys with the values on your dashboard.

PUSHER_APP_*键替换为仪表板上的值。

  • setupRecyclerView assigns a layout manager and an adapter to the recycler view. For a recycler view to work, you need these two things.

    setupRecyclerView将布局管理器和适配器分配给回收者视图。 为了使回收器视图正常工作,您需要这两件事。

  • fetchUsers fetches all the users from the server and displays on the list. It exempts the current user logged in.

    fetchUsers从服务器获取所有用户,并显示在列表中。 它免除了当前登录的用户。

  • subcribeToChannel subscribes to a presence channel. When you subscribe to one, the onUsersInformationReceived gives you all the users subscribed to the channel including the current user. So, in that callback, we call the showUserOnline method in the adapter class so that the icon beside the user can be changed to signify that the user is online.

    subcribeToChannel订阅状态频道。 当您订阅一个频道时, onUsersInformationReceived会为您订阅该频道的所有用户,包括当前用户。 因此,在该回调中,我们在适配器类中调用showUserOnline方法,以便可以更改用户旁边的图标以表示该用户在线。

  • onUserClicked is called when a contact is selected. We pass the details of the user to the next activity called ChatRoom.

    选择联系人时,将调用onUserClicked 。 我们将用户的详细信息传递给下一个称为ChatRoom活动。

In the previous snippet, we used an extension function to transform the User object we receive from Pusher to our own UserModel object. Let’s define this extension.

在上一片段中,我们使用了扩展功能将从Pusher接收到的User对象转换为我们自己的UserModel对象。 让我们定义这个扩展。

Create a new class called Utils and paste this:

创建一个名为Utils的新类,并将其粘贴:

// File: ./app/src/main/java/com/example/messengerapp/Utils.kt    import com.pusher.client.channel.User    import org.json.JSONObject
fun User.toUserModel():UserModel{      val jsonObject = JSONObject(this.info)      val name = jsonObject.getString("name")      val numb = jsonObject.getInt("count")      return UserModel(this.id, name, numb)    }

Now, since we referenced a ChatRoom activity earlier in the onUserClicked method, let’s create it.

现在,由于我们之前在onUserClicked方法中引用了ChatRoom活动,因此让我们创建它。

Create a new activity called ChatRoom. The activity comes with a layout file activity_chat_room. Paste this into the layout file:

创建一个名为ChatRoom的新活动。 该活动带有布局文件activity_chat_room 。 将此粘贴到布局文件中:

// File: ./app/src/main/res/layout/activity_chat_room.xml    &lt;?xml version="1.0" encoding="utf-8"?>    <android.support.constraint.ConstraintLayout 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"      tools:context=".ChatRoom">
<android.support.v7.widget.RecyclerView        android:id="@+id/recyclerViewChat"        android:layout_width="match_parent"        android:layout_height="match_parent" />
<EditText        android:id="@+id/editText"        android:layout_width="0dp"        android:layout_height="wrap_content"        android:layout_margin="16dp"        android:hint="Enter a message"        app:layout_constraintBottom_toBottomOf="parent"        app:layout_constraintEnd_toStartOf="@+id/sendButton"        app:layout_constraintStart_toStartOf="parent" />
<android.support.design.widget.FloatingActionButton        android:id="@+id/sendButton"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_gravity="end|bottom"        android:layout_margin="16dp"        android:src="@android:drawable/ic_menu_send"        app:layout_constraintEnd_toEndOf="parent"        app:layout_constraintBottom_toBottomOf="parent" />
</android.support.constraint.ConstraintLayout>

The layout above contains a recycler view for the chat messages, an edit text to collect new messages, and a floating action button to send the message.

上面的布局包含聊天消息的回收者视图,用于收集新消息的编辑文本以及用于发送消息的浮动操作按钮。

Next, create a new class called ChatRoomAdapter and paste in the following:

接下来,创建一个名为ChatRoomAdapter的新类,并粘贴以下内容:

// File: ./app/src/main/java/com/example/messengerapp/ChatRoomAdapter.kt    import android.support.v7.widget.CardView    import android.support.v7.widget.RecyclerView    import android.view.LayoutInflater    import android.view.View    import android.view.ViewGroup    import android.widget.RelativeLayout    import android.widget.TextView    import java.util.*
class ChatRoomAdapter (private var list: ArrayList<MessageModel>)      : RecyclerView.Adapter<ChatRoomAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {        return ViewHolder(LayoutInflater.from(parent.context)            .inflate(R.layout.chat_item, parent, false))      }
override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(list[position])
override fun getItemCount(): Int = list.size
fun add(message: MessageModel) {        list.add(message)        notifyDataSetChanged()      }
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {        private val messageTextView: TextView = itemView.findViewById(R.id.text)        private val cardView: CardView = itemView.findViewById(R.id.cardView)
fun bind(message: MessageModel) = with(itemView) {          messageTextView.text = message.message          val params = cardView.layoutParams as RelativeLayout.LayoutParams          if (message.senderId==Singleton.getInstance().currentUser.id) {            params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT)          }        }      }    }

This adapter works in a similar fashion as the one we created earlier. One difference, though, is that the show online and offline methods are not needed here.

该适配器的工作方式与我们之前创建的适配器类似。 但是,一个区别是,此处不需要在线和离线显示方法。

Next, create another class — named MessageMode— and paste in this:

接下来,创建另一个类-名为MessageMode并粘贴以下内容:

// File: ./app/src/main/java/com/example/messengerapp/MessageModel.kt    data class MessageModel(val message: String, val senderId: String)

The chat_item layout used in the onCreateViewHolder method of the adapter class represents how each layout will look like. Create a new layout called chat_item and paste in this:

适配器类的onCreateViewHolder方法中使用的chat_item布局表示每种布局的外观。 创建一个名为chat_item的新布局,并粘贴以下内容:

// File: ./app/src/main/res/layout/chat_item.xml    &lt;?xml version="1.0" encoding="utf-8"?>    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"      xmlns:app="http://schemas.android.com/apk/res-auto"      android:layout_width="wrap_content"      android:layout_height="wrap_content"      android:layout_margin="16dp"      android:orientation="vertical">
<android.support.v7.widget.CardView        android:id="@+id/cardView"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_gravity="start"        app:cardCornerRadius="8dp"        app:cardUseCompatPadding="true">
<LinearLayout          android:layout_width="wrap_content"          android:layout_height="wrap_content"          android:gravity="start"          android:orientation="vertical"          android:padding="8dp">
<TextView            android:id="@+id/text"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:layout_gravity="center_vertical|start"            android:layout_marginBottom="4dp"            android:textStyle="bold" />
</LinearLayout>
</android.support.v7.widget.CardView>
</RelativeLayout>

更新ChatRoom类 (Updating the ChatRoom class)

Finally, open the ChatRoom activity class and paste in this:

最后,打开ChatRoom活动类并粘贴:

// File: ./app/src/main/java/com/example/messengerapp/ChatRoom.kt    import android.app.Activity    import android.os.Bundle    import android.support.v7.app.AppCompatActivity    import android.support.v7.widget.LinearLayoutManager    import android.util.Log    import android.view.View    import android.view.inputmethod.InputMethodManager    import com.pusher.client.Pusher    import com.pusher.client.PusherOptions    import com.pusher.client.channel.PrivateChannelEventListener    import com.pusher.client.util.HttpAuthorizer    import kotlinx.android.synthetic.main.activity_chat_room.*    import okhttp3.MediaType    import okhttp3.RequestBody    import org.json.JSONObject    import retrofit2.Call    import retrofit2.Callback    import retrofit2.Response    import java.lang.Exception    import java.util.*
class ChatRoom : AppCompatActivity() {
companion object {        const val EXTRA_ID = "id"        const val EXTRA_NAME = "name"        const val EXTRA_COUNT = "numb"      }
private lateinit var contactName: String      private lateinit var contactId: String      private var contactNumb: Int = -1      lateinit var nameOfChannel: String      val mAdapter = ChatRoomAdapter(ArrayList())
override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        setContentView(R.layout.activity_chat_room)        fetchExtras()        setupRecyclerView()        subscribeToChannel()        setupClickListener()      }    }

In this file, we declared constants used to send data to the activity through intents. We also initialized variables we will use later, like the adapter and the contact details. We then called some additional methods in the onCreatemethod. Let’s add them to theChatRoom class.

在此文件中,我们声明了用于通过意图将数据发送到活动的常量。 我们还初始化了稍后将使用的变量,例如适配器和联系方式。 然后,我们在onCreate方法中调用了一些其他方法。 让我们将它们添加到ChatRoom类中。

Add the fetchExtras method defined below to the class. The method gets the extras sent from the chatroom activity.

将下面定义的fetchExtras方法添加到该类中。 该方法获取聊天室活动发送的临时演员。

private fun fetchExtras() {      contactName = intent.extras.getString(ChatRoom.EXTRA_NAME)      contactId = intent.extras.getString(ChatRoom.EXTRA_ID)      contactNumb = intent.extras.getInt(ChatRoom.EXTRA_COUNT)    }

The next method is setupRecyclerView . This initializes the recycler view with an adapter and a layout manager. Paste this function into the same class as before:

下一个方法是setupRecyclerView 。 这将使用适配器和布局管理器初始化回收器视图。 将此函数粘贴到与以前相同的类中:

private fun setupRecyclerView() {      with(recyclerViewChat) {        layoutManager = LinearLayoutManager(this@ChatRoom)        adapter = mAdapter      }    }

The next method is subscribeToChannel . This method subscribes the user to a private channel with the selected contact. Paste the following code into the same class as before:

下一个方法是subscribeToChannel 。 此方法使用所选联系人将用户预订到私人频道。 将以下代码粘贴到与以前相同的类中:

private fun subscribeToChannel() {      val authorizer = HttpAuthorizer("http://10.0.2.2:5000/pusher/auth/private")      val options = PusherOptions().setAuthorizer(authorizer)      options.setCluster("PUSHER_APP_CLUSTER")
val pusher = Pusher("PUSHER_APP_KEY", options)      pusher.connect()
nameOfChannel = if (Singleton.getInstance().currentUser.count > contactNumb) {        "private-" + Singleton.getInstance().currentUser.id + "-" + contactId      } else {        "private-" + contactId + "-" + Singleton.getInstance().currentUser.id      }
Log.i("ChatRoom", nameOfChannel)
pusher.subscribePrivate(nameOfChannel, object : PrivateChannelEventListener {        override fun onEvent(channelName: String?, eventName: String?, data: String?) {          val obj = JSONObject(data)          val messageModel = MessageModel(obj.getString("message"), obj.getString("sender_id"))
runOnUiThread {            mAdapter.add(messageModel)          }        }
override fun onAuthenticationFailure(p0: String?, p1: Exception?) {}        override fun onSubscriptionSucceeded(p0: String?) {}      }, "new-message")    }

Replace the PUSHER_APP_* keys with the values on your dashboard.

PUSHER_APP_*键替换为仪表板上的值。

The code above allows a user to subscribe to a private channel. A private channel requires authorization like the presence channel. However, it does not expose a callback that is triggered when other users subscribe.

上面的代码允许用户订阅私人频道。 专用频道需要与状态频道一样的授权。 但是,它不会公开其他用户订阅时触发的回调。

Next method to be added is setupClickListener. Paste the method into the same class as before:

下一个要添加的方法是setupClickListener 。 将方法粘贴到与以前相同的类中:

private fun setupClickListener() {      sendButton.setOnClickListener{        if (editText.text.isNotEmpty()) {          val jsonObject = JSONObject()          jsonObject.put("message",editText.text.toString())          jsonObject.put("channel_name",nameOfChannel)          jsonObject.put("sender_id",Singleton.getInstance().currentUser.id)
val jsonBody = RequestBody.create(              MediaType.parse("application/json; charset=utf-8"),               jsonObject.toString()          )
RetrofitInstance.retrofit.sendMessage(jsonBody).enqueue(object: Callback<String>{            override fun onFailure(call: Call<String>?, t: Throwable?) {}            override fun onResponse(call: Call<String>?, response: Response<String>?) {}          })
editText.text.clear()          hideKeyBoard()        }
}    }

The method above assigns a click listener to the floating action button to send the message to the server. After the message is sent, we clear the text view and hide the keyboard.

上面的方法将单击侦听器分配给浮动操作按钮,以将消息发送到服务器。 发送消息后,我们清除文本视图并隐藏键盘。

Add a method to the same class for hiding the keyboard like this:

将方法添加到同一类中以隐藏键盘,如下所示:

private fun hideKeyBoard() {      val imm = getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager      var view = currentFocus
if (view == null) {        view = View(this)      }
imm.hideSoftInputFromWindow(view.windowToken, 0)    }

That’s all for the application. Now you can run your application in Android Studio and you should see the application in action.

这就是应用程序的全部内容。 现在,您可以在Android Studio中运行您的应用程序,并且您应该会看到该应用程序的运行情况。

Make sure the Node.js API we built earlier is running before running the Android application.

在运行Android应用程序之前,请确保我们先前构建的Node.js API正在运行。

结论 (Conclusion)

In this article, you have been introduced to some Pusher capabilities such as the private and presence channel.

在本文中,向您介绍了一些Pusher功能,例如专用和状态通道。

We learned how to authenticate our users for the various channels.

我们学习了如何验证各种渠道的用户身份。

We used these channels to implement a private chat between two persons and an online notification for a contact.

我们使用这些渠道来实现两个人之间的私人聊天以及联系人的在线通知。

The source code to the application built in this article is available on GitHub.

GitHub上提供了本文构建的应用程序的源代码。

This post first appeared on the Pusher Blog.

这篇文章首先出现在Pusher Blog上 。

翻译自: https://www.freecodecamp.org/news/how-to-build-an-android-messenger-app-with-online-presence-using-kotlin-fdcb3ea9e73b/

如何使用Kotlin构建具有在线状态的Android Messenger应用相关推荐

  1. Android:《Kotlin 从零到精通Android开发》读书笔记

    原文发布在我的公众号:CnPeng 所有文章将优先发布于公众号,随后才会更新简书. 前前后后整整四十天,终于利用非工作时间读完了 欧阳燊(shen)写的 <Kotlin 从零到精通Android ...

  2. kotlin 垂直滚动_在Android的Kotlin中检测点击或滚动

    kotlin 垂直滚动 Build a responsive UI that shows or hides the toolbar in response to user clicks whilst ...

  3. kotlin 构建对象_使用Kotlin,TypeScript和Okta构建安全的Notes应用程序

    kotlin 构建对象 I love my job as a developer advocate at Okta. I get to learn a lot, write interesting b ...

  4. 坑中速记整理! 使用 kotlin 写第一个 ReactNative Android 模块

    Kotlin 和 Swift, 两大新宠! 借 ReactNative 熟悉下 kotlin 的用法,不料掉坑里面了.昨晚花了大半夜,趁这会儿思路清晰,把涉及到的一些关键信息,迅速整理下. 最佳的使用 ...

  5. android蓝牙通信_使用Arduino构建OLED显示屏与Android手机接口的智能手表

    背景知识视频教程 Arduino 训练营:通过项目学习​viadean.com 通过制作Arduino UNO FM收音机接收器学习Arduino I2C​viadean.com 通过构建实际应用程序 ...

  6. android 环形时间显示_使用Arduino构建OLED显示屏与Android手机接口的智能手表

    背景知识视频教程 Arduino 训练营:通过项目学习​viadean.com 通过制作Arduino UNO FM收音机接收器学习Arduino I2C​viadean.com 通过构建实际应用程序 ...

  7. 暴力突破 Gradle 自动化项目构建(八)- Android 对 Gradle 的扩展:Variants、Transform

    一.前言 上一篇学习了如何自定义 Gradle 插件,本篇我们来学习下 Android 对 Gradle 的扩展:Variants(变体)以及 Transform.通过扩展可以让我们在自定义 Grad ...

  8. 四针角oled屏连接arduino_使用Arduino构建OLED显示屏与Android手机接口的智能手表

    背景知识视频教程 Arduino 训练营:通过项目学习​viadean.com通过制作Arduino UNO FM收音机接收器学习Arduino I2C​viadean.com通过构建实际应用程序来掌 ...

  9. kotlin与java混合开发_使用kotlin和Java混合开发Android项目爬坑记录

    使用kotlin和Java混合开发Android项目爬坑记录 不定期将开发过程中遇到的问题添加在此处,加深记忆. 主要内容包括但不限于java与kotlin语言的一些区别,以及在使用android-s ...

最新文章

  1. 预计2020年传感器需求超一万亿个
  2. 教你用Python解决非平衡数据问题(附代码)
  3. 怎么调用系统通讯录并向被选中联系人发送短信
  4. Django REST framework 版本
  5. python django flask介绍_django和flask哪个值得研究学习
  6. linux .net 控制台应用程序,VisualStudioCode创建的asp.net core控制台程序部署到linux
  7. 修改注册表解决SVN状态图标不显示问题
  8. Jmeter (二十五)逻辑控制器 之 Random Controller and Random order Controller
  9. 厉害了!如何在 Gihub 快速搜索开源项目?
  10. mysql 5.5.32备份数据库_十六、mysql的备份与恢复(二)--mysqldump
  11. 秒变金庸风 | NLP文本风格迁移
  12. 一文读懂人脸识别技术
  13. 电子设计大赛-AD与DA电路设计
  14. 分库分表中间件的高可用实践讲解
  15. 12.计算机网络---iptables防火墙管理工具
  16. 放慢脚步是为了走得更快
  17. 一对一直播源码是什么?一对一直播平台如何成功搭建?
  18. Win11开机时一直转圈是什么问题
  19. linux怎么进入绘图模式,Linux 绘图工具
  20. Redis实战:第五章-使用Redis构建支持程序

热门文章

  1. 一步搞定你疑惑的数据结构与算法系列,原理+实战讲解
  2. 阿里P7亲自教你!我的头条面试经历分享,完整PDF
  3. react typescript 子组件调用父组件
  4. 伯努利数学习笔记的说...
  5. 关于position的四个标签
  6. ubuntu opengl 安装
  7. go map数据结构
  8. 8-python自动化-day08-进程、线程、协程篇
  9. Mysql中natural join和inner join的区别
  10. Ubuntu安装qwt步骤