前提条件
安装并配置好Android Studio
Android Studio Electric Eel | 2022.1.1 Patch 2
Build #AI-221.6008.13.2211.9619390, built on February 17, 2023
Runtime version: 11.0.15+0-b2043.56-9505619 amd64
VM: OpenJDK 64-Bit Server VM by JetBrains s.r.o.
Windows 11 10.0
GC: G1 Young Generation, G1 Old Generation
Memory: 1280M
Cores: 6
Registry:
external.system.auto.import.disabled=true
ide.text.editor.with.preview.show.floating.toolbar=false
ide.balloon.shadow.size=0
Non-Bundled Plugins:
com.intuit.intellij.makefile (1.0.15)
com.github.setial (4.0.2)
com.alayouni.ansiHighlight (1.2.4)
GsonOrXmlFormat (2.0)
GLSL (1.19)
com.mistamek.drawablepreview.drawable-preview (1.1.5)
com.layernet.plugin.adbwifi (1.0.5)
com.likfe.ideaplugin.eventbus3 (2020.0.2)gradle-wrapper.properties
#Tue Apr 25 13:34:44 CST 2023
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
build.gradle(:Project)
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '7.3.1' apply false
id 'com.android.library' version '7.3.1' apply false
id 'org.jetbrains.kotlin.android' version '1.7.20' apply false
}
setting.gradle
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
maven { url 'https://jitpack.io' }
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
gradlePluginPortal()
maven { url 'https://jitpack.io' }
}
}
rootProject.name = "logindemo"
include ':app'
build.gralde(:app)
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'kotlin-android'
id 'kotlin-kapt'
}
android {
namespace 'com.example.fechat'
compileSdk 33
defaultConfig {
applicationId "com.example.fechat"
minSdk 26
targetSdk 33
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.8.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
// 沉浸式状态栏 https://github.com/gyf-dev/ImmersionBar
implementation 'com.gyf.immersionbar:immersionbar:3.0.0'
implementation 'com.gyf.immersionbar:immersionbar-components:3.0.0' // fragment快速实现(可选)
implementation 'com.gyf.immersionbar:immersionbar-ktx:3.0.0' // kotlin扩展(可选)
implementation 'com.google.code.gson:gson:2.8.9'
implementation "androidx.room:room-runtime:2.4.2"
implementation "androidx.room:room-ktx:2.4.2"
kapt "androidx.room:room-compiler:2.4.2"
implementation 'org.apache.commons:commons-csv:1.5'
implementation 'com.permissionx.guolindev:permissionx:1.4.0'
implementation 'com.blankj:utilcodex:1.30.0' // 无
implementation 'com.github.bumptech.glide:glide:4.12.0'
kapt 'com.github.bumptech.glide:compiler:4.12.0'
}
对Kotlin语言有基本了解
内容在前一篇博客中写了基础配置,如果本篇内容看不懂,可以先去上一篇。
数据本地化方案
采用room数据库保存首页用户聊天列表,为此引入room库
implementation "androidx.room:room-runtime:2.4.2"
implementation "androidx.room:room-ktx:2.4.2"
kapt "androidx.room:room-compiler:2.4.2"
采用csv文件来按行保存与用户聊天内容,这里csv不具有任何数据保护性,所以如果想要实现本地化并且数据加密,可以对保存在csv文件中的数据进行加密处理,读取时只需要解密即可还原,为此引入了kotlin的CSV读写库
implementation 'org.apache.commons:commons-csv:1.5'
其他优秀的开源库在这里也一起引入了,在这里感谢各位开源库作者
权限申请库
implementation 'com.permissionx.guolindev:permissionx:1.4.0'
通用工具类
implementation 'com.blankj:utilcodex:1.30.0' // 无
加载图片的glide库
implementation 'com.github.bumptech.glide:glide:4.12.0'
kapt 'com.github.bumptech.glide:compiler:4.12.0'
本地化实现过程中新增的代码过多,所以不便这里一一贴出来,感兴趣的同学请移步开源库
FeChat: 模仿微信
首页聊天页面中的数据刷新
package com.example.fechat.fragment
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.fechat.R
import com.example.fechat.activity.MessageActivity
import com.example.fechat.base.BaseAdapter
import com.example.fechat.room.user.UserDBUtils
import com.example.fechat.room.user.UserEntity
import java.util.*
class ChatFragment : Fragment() {
private var baseAdapter: BaseAdapter? = null
private lateinit var recyclerView: RecyclerView
private var data: List<UserEntity>? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_chat, container, false)
recyclerView = view.findViewById(R.id.recyclerView)
recyclerView.layoutManager = LinearLayoutManager(context)
data =
ArrayList(UserDBUtils.getAll(context)).filter { it.last_message.isNotEmpty() }
data?.let {
Collections.sort(it) { o1, o2 ->
(o2.duration - o1.duration).toInt()
}
}
baseAdapter = BaseAdapter(data!!)
recyclerView.adapter = baseAdapter
baseAdapter?.setOnItemClickListener(object : BaseAdapter.OnItemClickListener {
override fun onItemClick(view: View, position: Int) {
val intent = Intent(context, MessageActivity::class.java)
intent.putExtra("UserInfo", data!![position].toString())
startActivity(intent)
}
})
return view
}
fun resume() {
data =
ArrayList(UserDBUtils.getAll(context)).filter { it.last_message.isNotEmpty() }
data?.let {
Collections.sort(it) { o1, o2 ->
(o2.duration - o1.duration).toInt()
}
}
baseAdapter?.setNewData(data!!)
}
}
其中UserDBUtils是数据库接口,读取保存的聊天记录(包含用户名和最新的聊天记录)
而Collections.sort是对读出来的聊天记录(多用户)按照最新聊天记录的时间进行的排序
聊天页面数据本地化
package com.example.fechat.activity
import android.annotation.SuppressLint
import android.os.Bundle
import android.widget.EditText
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.blankj.utilcode.util.FileUtils
import com.example.fechat.R
import com.example.fechat.adapter.ChatAdapter
import com.example.fechat.bean.MessageBean
import com.example.fechat.room.user.UserDBUtils
import com.example.fechat.room.user.UserEntity
import com.example.fechat.utils.CSVUtils
import com.google.gson.Gson
import com.gyf.immersionbar.ImmersionBar
class MessageActivity : AppCompatActivity() {
private val beans = ArrayList<MessageBean>()
private var adapter: ChatAdapter? = null
private lateinit var itemView: RecyclerView
private lateinit var userEntity: UserEntity
private var messagePath = ""
private val userName = "Admin"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ImmersionBar.with(this).statusBarDarkFont(true).statusBarColor(R.color.title)
.navigationBarColor(R.color.white).navigationBarDarkIcon(true).init()
setContentView(R.layout.activity_message)
val backTv = findViewById<TextView>(R.id.backTv)
val inputText: EditText = findViewById(R.id.inputText)
val sendText: TextView = findViewById(R.id.sendText)
val userName: TextView = findViewById(R.id.userName)
backTv.setOnClickListener {
finish()
}
sendText.setOnClickListener {
sendText(inputText.text.toString())
inputText.setText("")
}
getBundle()
initItemRecyclerView()
userName.text = userEntity.userName
}
private fun getBundle() {
val userInfo = intent.getStringExtra("UserInfo")
userEntity = Gson().fromJson(userInfo, UserEntity::class.java)
messagePath = CSVUtils.getPath(this, userEntity.userId)
FileUtils.createOrExistsFile(messagePath)
}
override fun onResume() {
super.onResume()
beans.addAll(CSVUtils.readFromCSV(messagePath))
}
private fun initItemRecyclerView() {
itemView = findViewById(R.id.itemView)
val layoutManager = LinearLayoutManager(this)
layoutManager.orientation = RecyclerView.VERTICAL
itemView.layoutManager = layoutManager
adapter = ChatAdapter(beans, userEntity)
itemView.adapter = adapter
}
@SuppressLint("NotifyDataSetChanged")
private fun sendText(message: String) {
insertMessage(message)
adapter?.notifyDataSetChanged()
}
private fun insertMessage(message: String) {
val messageBean = MessageBean(
message, userName, false, System.currentTimeMillis(), true
)
beans.add(messageBean)
CSVUtils.writeToCSV(messageBean, messagePath)
val messageBeanResp =
MessageBean(message, userEntity.userName, true, System.currentTimeMillis(), true)
beans.add(messageBeanResp)
CSVUtils.writeToCSV(messageBeanResp, messagePath)
userEntity.duration = System.currentTimeMillis()
userEntity.last_message = message
UserDBUtils.insertUser(this, userEntity)
}
}
数据本地化包括对首页聊天记录列表的更新和单用户聊天记录保存到CSV文件中。
CSV工具类
package com.example.fechat.utils
import android.content.Context
import com.example.fechat.bean.MessageBean
import org.apache.commons.csv.CSVFormat
import org.apache.commons.csv.CSVParser
import org.apache.commons.csv.CSVPrinter
import java.io.*
object CSVUtils {
fun getPath(context: Context, userId: String): String {
return "${context.getExternalFilesDir(null)}/message/${userId}.csv"
}
fun writeToCSV(bean: MessageBean, path: String) {
val bufferWrite = BufferedWriter(OutputStreamWriter(FileOutputStream(path, true)))
val csvPrinter = CSVPrinter(bufferWrite, CSVFormat.DEFAULT)
val data = listOf(bean.message, bean.userName, bean.isResponse, bean.time, bean.isSuccess)
csvPrinter.printRecord(data)
csvPrinter.flush()
csvPrinter.close()
}
fun readFromCSV(path: String): ArrayList<MessageBean> {
val bufferedReader = BufferedReader(FileReader(File(path)))
val csvParser = CSVParser(bufferedReader, CSVFormat.DEFAULT)
val messageBeans = ArrayList<MessageBean>()
csvParser.forEach { parse ->
val messageBean = MessageBean(
parse[0],
parse[1],
parse[2].toBoolean(),
parse[3].toLong(),
parse[4].toBoolean()
)
messageBeans.add(messageBean)
}
return messageBeans
}
}
新增的权限请求
package com.example.fechat.activity
import android.Manifest
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.provider.Settings
import android.widget.BaseAdapter
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentPagerAdapter
import androidx.viewpager.widget.ViewPager
import com.example.fechat.R
import com.example.fechat.fragment.ChatFragment
import com.example.fechat.fragment.ContactsFragment
import com.example.fechat.fragment.DiscoverFragment
import com.google.android.material.tabs.TabLayout
import com.gyf.immersionbar.ImmersionBar
import com.permissionx.guolindev.PermissionX
class MainActivity : AppCompatActivity() {
private lateinit var viewPager: ViewPager
private lateinit var tabLayout: TabLayout
private lateinit var titleTv: TextView
private val fragments = ArrayList<Fragment>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ImmersionBar.with(this)
.statusBarDarkFont(true)
.statusBarColor(R.color.title)
.navigationBarColor(R.color.white)
.navigationBarDarkIcon(true)
.init()
setContentView(R.layout.activity_main)
initPermission()
}
private fun initPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (!Environment.isExternalStorageManager()) {
val intent = Intent()
intent.action = Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION
val uri: Uri = Uri.fromParts("package", this.packageName, null)
intent.data = uri
startActivityForResult(intent, 0x99)
} else {
initView()
}
} else {
PermissionX.init(this)
.permissions(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
.request { allGranted, _, _ ->
if (allGranted) {
initView()
}
}
}
}
private fun initView() {
fragments.addAll(
listOf(
ChatFragment(),
ContactsFragment(),
DiscoverFragment()
)
)
titleTv = findViewById(R.id.titleTv)
viewPager = findViewById(R.id.viewPager)
tabLayout = findViewById(R.id.tabLayout)
viewPager.adapter = ViewPagerAdapter(supportFragmentManager, fragments)
tabLayout.setupWithViewPager(viewPager)
tabLayout.getTabAt(0)?.text = "聊天"
tabLayout.getTabAt(1)?.text = "联系人"
tabLayout.getTabAt(2)?.text = "发现"
tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
titleTv.text = tab?.text
if (tab?.position == 0) {
(fragments[0] as ChatFragment).resume()
}
}
override fun onTabUnselected(tab: TabLayout.Tab?) {
}
override fun onTabReselected(tab: TabLayout.Tab?) {
}
})
}
class ViewPagerAdapter(
fragmentManager: androidx.fragment.app.FragmentManager,
private val fragments: List<Fragment>
) : FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
override fun getItem(position: Int): Fragment {
return fragments[position]
}
override fun getCount(): Int {
return fragments.size
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == 0x99) {
initPermission()
}
}
}
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:icon="@drawable/icon_logo"
android:roundIcon="@drawable/icon_logo"
android:label="@string/app_name"
android:supportsRtl="true"
android:name=".base.BaseApplication"
android:theme="@style/Theme.FeChat.Font"
tools:targetApi="31">
<activity
android:name=".activity.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".activity.MessageActivity" />
</application>
</manifest>
新增字体
字体是阿里开源的,仅供大家学习使用,商用可能产生版权纠纷
themes.xml
<style name="Theme.FeChat.Font" parent="Theme.FeChat">
<item name="fontFamily">@font/alimama_dongfangdakai_regular</item>
</style>
字体引用
<application
android:allowBackup="true"
android:icon="@drawable/icon_logo"
android:roundIcon="@drawable/icon_logo"
android:label="@string/app_name"
android:supportsRtl="true"
android:name=".base.BaseApplication"
android:theme="@style/Theme.FeChat.Font"
tools:targetApi="31">
<activity
android:name=".activity.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".activity.MessageActivity" />
</application>