您现在的位置是:首页 >技术杂谈 >Android类似微信聊天页面教程(Kotlin)四——数据本地化网站首页技术杂谈

Android类似微信聊天页面教程(Kotlin)四——数据本地化

不会写代码的猴子 2023-06-18 00:00:03
简介Android类似微信聊天页面教程(Kotlin)四——数据本地化

 

前提条件

安装并配置好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>

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。