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