prerequisite
Install and configure 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'
}
Basic understanding of the Kotlin language
The basic configuration was written in the previous blog. If you don’t understand the content of this article, you can go to the previous article first.
Data Localization Scheme
Use the room database to save the user chat list on the home page, and introduce the room library for this
implementation "androidx.room:room-runtime:2.4.2"
implementation "androidx.room:room-ktx:2.4.2"
kapt "androidx.room:room-compiler:2.4.2"
The csv file is used to save the chat content with the user line by line. The csv does not have any data protection, so if you want to achieve localization and data encryption, you can encrypt the data stored in the csv file. When reading, you only need Decryption can be restored, so kotlin's CSV read and write library is introduced
implementation 'org.apache.commons:commons-csv:1.5'
Other excellent open source libraries are also introduced here, thanks to the authors of open source libraries
Permission Application Library
implementation 'com.permissionx.guolindev:permissionx:1.4.0'
General tools
implementation 'com.blankj:utilcodex:1.30.0' // 无
glide library for loading images
implementation 'com.github.bumptech.glide:glide:4.12.0'
kapt 'com.github.bumptech.glide:compiler:4.12.0'
There are too many new codes in the process of localization implementation, so it is inconvenient to post them here one by one. Interested students, please move to the open source library
Data Refresh in Home Chat Page
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!!)
}
}
Among them, UserDBUtils is the database interface, which reads the saved chat records (including user names and latest chat records)
And Collections.sort sorts the read chat records (multi-user) according to the time of the latest chat records
Chat page data localization
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)
}
}
Data localization includes updates to the front page chat history list and saving single-user chat history to CSV files.
CSV Tools
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
}
}
Added permission request
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>
new font
The font is open sourced by Ali, and it is only for everyone to learn and use. Commercial use may cause copyright disputes
themes.xml
<style name="Theme.FeChat.Font" parent="Theme.FeChat">
<item name="fontFamily">@font/alimama_dongfangdakai_regular</item>
</style>
font reference
<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>