Android similar WeChat chat page tutorial (Kotlin) IV - data localization

 

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

FeChat: Imitation of WeChat

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>

Guess you like

Origin blog.csdn.net/mozushixin_1/article/details/130390958