Android Kotlin学习 Jitpack 组件之Room和ViewModel

前言

今天开始来学习 Kotlin,毫无疑问,今后的Android开发将会越来越往 Kotlin 靠拢,谷歌也明确声明今后会优先支持 Kotlin 的维护更新。那总得接受新事物的,刚好最近开了IO大会,看了下来,感觉很多接口也挺有意思,确实更加方便,简约代码量。

接下来会以一个小小的简约版 记账本 app作为接入点来进行学习,并且将我个人的学习过程记录下来,与各位分享。如有错误的地方,望见谅。
————————————————

书接上回,各位朋友可以根据下方相关链接回看详情。

相关链接

Android Kotlin学习 Jitpack 组件之DataBinding

添加依赖

在build.gradle文件中添加配置信息

android {
    // other configuration

    packagingOptions {
        exclude 'META-INF/atomicfu.kotlin_module'
    }
}
dependencies {
    ...

    // Room components
    implementation "androidx.room:room-runtime:2.2.0"
    implementation "androidx.room:room-ktx:2.2.0"
    kapt "androidx.room:room-compiler:2.2.0"
    androidTestImplementation "androidx.room:room-testing:2.2.0"
    // Lifecycle components
    implementation "androidx.lifecycle:lifecycle-extensions:2.2.0-beta01"
    kapt "androidx.lifecycle:lifecycle-compiler:2.2.0-beta01"
    androidTestImplementation "androidx.arch.core:core-testing:2.1.0"
    // ViewModel Kotlin support
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-beta01"
    // Coroutines
    api "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0"
    api "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0"
}

创建实体类

创建一个名为【entity】的文件夹
在这里插入图片描述
在文件夹中创建文件【BillInfo】
在这里插入图片描述
编写账单实体类内容,主要做了以下几个内容:
1、设置数据库的表名为–bill_table
2、设置一个自增长ID
3、根据账本所需要的参数设计信息
4、金额使用String类型保存,避免Float或Double型转换失真

/**
 * 账单信息
 *
 * @author D10NG
 * @date on 2019-10-17 14:58
 */
@Entity(tableName = "bill_table")
data class BillInfo(
    // 账单ID,主键,自增长
    @PrimaryKey(autoGenerate = true)
    var id : Long = -1 ,
    // 账单项目名称
    var name : String = "",
    // 是否为支出
    var isPay : Boolean = false,
    // 金额
    var number : String = "0",
    // 年
    var year : Int = 0,
    // 月
    var month : Int = 0,
    // 日
    var day : Int = 0,
    // 上一次修改时间
    var lastTime : Long = 0
)

更多实体创建相关教程可查看:
Defining data using Room entities

创建数据库操作接口DAO

创建一个名为【dao】的文件夹
在这里插入图片描述
在文件夹中创建文件【BillDao】
在这里插入图片描述
编写数据库操作接口内容,主要有以下几个要点:
1、定义BillDao为接口类型,并添加@Dao注解
2、由于我们的首页会显示一天的账单信息,所以,需要一个查询一天的账单列表的方法(按修改时间倒序返回),而且返回的数据应该是LiveData格式,可被观察的数据。
3、我们还需要插入新账单、编辑账单、删除账单的方法
4、修改账单的时候,遇到信息没有修改时,应该忽略
5、使用suspend修饰词,将耗时操作挂起

/**
 * 数据表操作接口
 *
 * @author D10NG
 * @date on 2019-10-17 15:20
 */
@Dao
interface BillDao {

    // 查询当日所有账单
    @Query("SELECT * FROM bill_table WHERE year = (:year) AND month = (:month) AND day = (:day) ORDER BY lastTime desc")
    fun getDayAll(year : Int, month : Int, day : Int) : LiveData<List<BillInfo>>

    // 插入新账单
    @Insert
    suspend fun insert(info : BillInfo)

    // 修改账单,相同就跳过
    @Update(onConflict = OnConflictStrategy.IGNORE)
    suspend fun update(info: BillInfo)

    // 删除账单
    @Delete
    suspend fun delete(info: BillInfo)
}

更多创建DAO相关教程:
Accessing data using Room DAOs

创建数据库管理RoomDatabase

创建一个名为【database】的文件夹
在这里插入图片描述
在文件夹中创建文件【BillDatabase】
在这里插入图片描述
编写文件内容,主要有以下几个要点:
1、声明操作实体类和操作接口
2、声明版本号
3、创建数据库名为“bill_db”的实例

/**
 * 数据库管理
 *
 * @author D10NG
 * @date on 2019-10-17 16:04
 */
@Database(entities = [BillInfo::class], version = 1)
abstract class BillDatabase : RoomDatabase() {

    abstract fun getBillDao() : BillDao

    companion object {

        // 单例
        @Volatile
        private var INSTANCE : BillDatabase? = null

        fun getDatabase(context : Context) : BillDatabase {
            val temp = INSTANCE
            if (null != temp) {
                return temp
            }
            synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    BillDatabase::class.java,
                    "bill_db"
                ).build()
                INSTANCE = instance
                return instance
            }
        }
    }
}

创建ViewModel

创建一个名为【model】的文件夹
在这里插入图片描述
在文件夹中创建文件【MainViewModel】
在这里插入图片描述
编写文件内容,主要有以下几个要点:
1、使用viewModelScope.launch保证当ViewModel被销毁时会自动取消正在执行任务

/**
 * 页面数据管理器
 *
 * @author D10NG
 * @date on 2019-10-17 16:25
 */
class MainViewModel(application: Application) : AndroidViewModel(application) {

    private val db : BillDatabase = BillDatabase.getDatabase(application)

    private var allDayBill : LiveData<List<BillInfo>>

    init {
        allDayBill = db.getBillDao().getDayAll(2019, 10, 18)
    }

    fun getAllDayBill() : LiveData<List<BillInfo>> = allDayBill

    fun updateAllDayBill(year : Int, month : Int, day : Int) {
        allDayBill = db.getBillDao().getDayAll(year, month, day)
    }

    fun insertBill(info : BillInfo) = viewModelScope.launch{
        db.getBillDao().insert(info)
    }

    fun updateBill(info: BillInfo) = viewModelScope.launch {
        db.getBillDao().update(info)
    }

    fun deleteBill(info: BillInfo) = viewModelScope.launch {
        db.getBillDao().delete(info)
    }
}

创建item布局

新建一个xml,命名为【item_bill.xml】
【data】中就一个数据,就是账单信息实体,然后显示账单名称和账单金额

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable
            name="bill"
            type="com.dlong.kotlintest.entity.BillInfo" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:orientation="horizontal">

            <TextView
                android:id="@+id/txt_name"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_marginLeft="20dp"
                android:layout_weight="1"
                android:gravity="center_vertical"
                android:text="@{bill.name}"
                android:textColor="#666666" />

            <TextView
                android:id="@+id/txt_number"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:gravity="center_vertical"
                android:text='@{(bill.pay? "-" : "") + bill.number}'
                android:textColor="#666666" />

            <ImageButton
                android:id="@+id/btn_menu"
                style="@style/Base.Widget.AppCompat.Button.Borderless"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:adjustViewBounds="true"
                android:scaleType="centerInside"
                app:srcCompat="@mipmap/icon_more" />
        </LinearLayout>

        <ImageView
            android:layout_width="match_parent"
            android:layout_height="1px"
            app:srcCompat="@android:color/darker_gray" />

    </LinearLayout>
</layout>

创建列表Adapter

创建一个名为【adapter】的文件夹
在这里插入图片描述
在文件夹中创建文件【BillListAdapter】
在这里插入图片描述
编写文件内容,主要有以下几个要点:
1、adapter需要传入两个参数,Context 和 Handler
2、同样使用DataBinding来绑定控件
3、点击按钮后显示菜单弹窗,并将菜单弹窗的选择回调到activity中处理

/**
 * 列表适配器
 *
 * @author D10NG
 * @date on 2019-10-18 09:35
 */
class BillListAdapter internal constructor(
    private val context : Context,
    private val handler : Handler
) : RecyclerView.Adapter<BillListAdapter.ViewHolder>() {

    companion object {
        const val CLICK_MENU_EDIT = 1001
        const val CLICK_MENU_DELETE = 1002
    }

    private val inflater: LayoutInflater = LayoutInflater.from(context)
    private var mList : List<BillInfo> = emptyList()

    internal fun update(list : List<BillInfo>) {
        this.mList = list
        notifyDataSetChanged()
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val binding : ItemBillBinding = DataBindingUtil.inflate(inflater, R.layout.item_bill, parent, false)
        return ViewHolder(binding)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(mList[position])
        holder.getBind().btnMenu.setOnClickListener { view ->
            // 显示弹窗
            val pop = PopupMenu(context, view)
            pop.menuInflater.inflate(R.menu.menu_bill, pop.menu)
            pop.setOnMenuItemClickListener {
                val m : Message = obtain()
                if (it.itemId == R.id.edit) {
                    m.what = Companion.CLICK_MENU_EDIT
                } else{
                    m.what = CLICK_MENU_DELETE
                }
                m.arg1 = position
                m.obj = mList[position]
                handler.sendMessage(m)
                true
            }
            pop.show()
        }
    }

    override fun getItemCount(): Int = mList.size

    inner class ViewHolder(private val binding: ItemBillBinding) : RecyclerView.ViewHolder(binding.root) {

        fun bind(data : BillInfo) {
            binding.bill = data
            binding.executePendingBindings()
        }

        fun getBind() : ItemBillBinding = binding
    }
}

修改activity里的逻辑

class MainActivity : AppCompatActivity(), BaseHandler.BaseHandlerCallBack {

    private lateinit var binding : ActivityMainBinding
    private lateinit var adapter : BillListAdapter
    private lateinit var viewModel : MainViewModel

    private val mHandler = BaseHandler(this, this)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 声明绑定
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        // 初始化ViewModel
        viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
        // 显示当前年月
        binding.year = DateUtils.curYear
        binding.month = DateUtils.curMonth
        binding.day = DateUtils.curDay
        // 更新账单列表数据
        updateAllBill()
        // 显示日期选项
        selectDay()
        // tab选择
        binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
            override fun onTabSelected(tab: TabLayout.Tab) {
                binding.day = binding.tabLayout.selectedTabPosition + 1
                updateAllBill()
            }
            override fun onTabUnselected(tab: TabLayout.Tab) { }
            override fun onTabReselected(tab: TabLayout.Tab) { }
        })
        // 初始化adapter
        adapter = BillListAdapter(this, mHandler)
        // 初始化rcv
        binding.rcv.layoutManager = LinearLayoutManager(this)
        binding.rcv.adapter = adapter

        // 日历按钮点击事件
        binding.btnCalendar.setOnClickListener {
            val dialog = DatePickerDialog(this,
                DatePickerDialog.OnDateSetListener { datePicker, y, m, d ->
                    binding.year = y
                    binding.month = m + 1
                    binding.day = d
                    selectDay()
                    updateAllBill()
                }, binding.year, binding.month -1, binding.day)
            dialog.show()
        }
        // btn点击事件
        binding.fab.setOnClickListener {
            Toast.makeText(this, "点击了fab", Toast.LENGTH_SHORT).show()
            val info = BillInfo()
            info.id = System.currentTimeMillis()
            info.name = "测试_${info.id}"
            info.number = "121.01"
            info.isPay = true
            info.lastTime = System.currentTimeMillis()
            info.year = binding.year
            info.month = binding.month
            info.day = binding.day
            viewModel.insertBill(info)
        }
    }

    override fun callBack(msg: Message) {
        when(msg.what) {
            BillListAdapter.CLICK_MENU_EDIT -> {
                // 编辑
            }
            BillListAdapter.CLICK_MENU_DELETE -> {
                // 删除
                val info : BillInfo = msg.obj as BillInfo
                viewModel.deleteBill(info)
            }
        }
    }

    private fun updateAllBill() {
        // 更新账单列表数据
        viewModel.updateAllDayBill(binding.year, binding.month, binding.day)
        // 监听列表变化
        viewModel.getAllDayBill().observe(this, Observer { list ->
            list?.let {
                adapter.update(it)
                var earn = 0f
                var pay = 0f
                for (i in it.listIterator()) {
                    if (i.isPay) {
                        pay += i.number.toFloat()
                    } else {
                        earn += i.number.toFloat()
                    }
                }
                binding.dayAllEarnValue = earn
                binding.dayAllPayValue = pay
            }
        })
    }

    private fun selectDay() {
        // 移除旧日期选项
        binding.tabLayout.removeAllTabs()
        // 填入新的日期选项
        for(i in 1..DateUtils.getDaysOfMonth(binding.year, binding.month)) {
            binding.tabLayout.addTab(binding.tabLayout.newTab().setText("${i}日"), false)
        }
        // 选中
        binding.tabLayout.getTabAt(binding.day - 1)?.select()
        // 转到对应位置
        binding.tabLayout.post {
            Thread.sleep(500)
            binding.tabLayout.setScrollPosition(0, (binding.day - 1).toFloat(), false)
        }
    }
}

BaseHandler 是一个为了避免内存泄漏的Handler子类

/**
 * 封装Handler子类
 * $ 解决handler内存泄漏问题
 *
 * @author D10NG
 * @date on 2019-09-28 11:11
 */
class BaseHandler(c: AppCompatActivity, private val callBack: BaseHandlerCallBack) : Handler() {

    private val act: WeakReference<AppCompatActivity> = WeakReference(c)

    override fun handleMessage(msg: Message) {
        super.handleMessage(msg)
        val c = act.get()
        if (c != null) {
            callBack.callBack(msg)
        }
    }

    interface BaseHandlerCallBack {
        fun callBack(msg: Message)
    }
}

下面是用到的两个工具类:
1、StringUtils

object StringUtils {

    /**
     * 由于Java是基于Unicode编码的,因此,一个汉字的长度为1,而不是2。
     * 但有时需要以字节单位获得字符串的长度。例如,“123abc长城”按字节长度计算是10,而按Unicode计算长度是8。
     * 为了获得10,需要从头扫描根据字符的Ascii来获得具体的长度。如果是标准的字符,Ascii的范围是0至255,如果是汉字或其他全角字符,Ascii会大于255。
     * 因此,可以编写如下的方法来获得以字节为单位的字符串长度。
     */
    fun getWordCount(s: String): Int {
        var length = 0
        for (i in s.indices) {
            val ascii = Character.codePointAt(s, i)
            if (ascii in 0..255)
                length++
            else
                length += 2

        }
        return length
    }

    /**
     * 补全length位,不够的在前面加0
     * @param str
     * @return
     */
    fun upToNString(str: String, length: Int): String {
        var result = StringBuilder()
        if (str.length < length) {
            for (i in 0 until length - str.length) {
                result.append("0")
            }
            result.append(str)
        } else {
            result = StringBuilder(str)
        }
        return result.toString()
    }

    /**
     * 补全length位,不够的在后面加0
     * @param str
     * @return
     */
    fun upToNStringInBack(str: String, length: Int): String {
        var result = StringBuilder()
        if (str.length < length) {
            result.append(str)
            for (i in 0 until length - str.length) {
                result.append("0")
            }
        } else {
            result = StringBuilder(str)
        }
        return result.toString()
    }
}

2、DateUtils

object DateUtils {

    /**
     * 获取系统时间戳
     * @return
     */
    val curTime: Long
        get() = System.currentTimeMillis()

    val curYear: Int
        get() = Integer.parseInt(getCurDateStr("yyyy"))

    val curMonth: Int
        get() = Integer.parseInt(getCurDateStr("MM"))

    val curDay: Int
        get() = Integer.parseInt(getCurDateStr("dd"))

    /**
     * 时间戳转换成字符窜
     * @param milSecond
     * @param pattern
     * @return
     */
    fun getDateStr(milSecond: Long, pattern: String): String {
        val date = Date(milSecond)
        @SuppressLint("SimpleDateFormat")
        val format = SimpleDateFormat(pattern)
        return format.format(date)
    }

    /**
     * 获取当前时间字符串
     * @param pattern
     * @return
     */
    fun getCurDateStr(pattern: String): String {
        return getDateStr(curTime, pattern)
    }

    /**
     * 将字符串转为时间戳
     * @param dateString
     * @param pattern
     * @return
     */
    fun getDateFromStr(dateString: String, pattern: String): Long {
        @SuppressLint("SimpleDateFormat")
        val dateFormat = SimpleDateFormat(pattern)
        var date: Date? = Date()
        try {
            date = dateFormat.parse(dateString)
        } catch (e: Exception) {
            e.printStackTrace()
        }

        return date?.time ?: 0
    }

    /**
     * 获取指定月份的天数
     * @param year
     * @param month
     * @return
     */
    fun getDaysOfMonth(year: Int, month: Int): Int {
        val calendar = Calendar.getInstance()
        try {
            val sdf = SimpleDateFormat("yyyy-MM-dd")
            calendar.time = sdf.parse(
                StringUtils.upToNString(year.toString() + "", 4) + "-" +
                        StringUtils.upToNString(month.toString() + "", 2) + "-01"
            )!!
        } catch (e: Exception) {
            e.printStackTrace()
        }

        return calendar.getActualMaximum(Calendar.DAY_OF_MONTH)
    }

    fun getDateYear(time: Long): Int {
        return Integer.parseInt(getDateStr(time, "yyyy"))
    }

    fun getDateMonth(time: Long): Int {
        return Integer.parseInt(getDateStr(time, "MM"))
    }

    fun getDateDay(time: Long): Int {
        return Integer.parseInt(getDateStr(time, "dd"))
    }
}

运行程序

效果图:
在这里插入图片描述
在这里插入图片描述

完事

完整demo
KotlinTest – Kotlin学习、DataBinding/Room/ViewModel学习demo

发布了103 篇原创文章 · 获赞 31 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/sinat_38184748/article/details/102606584