android java转kotlin + jetpack爬坑日记

编程入门 行业动态 更新时间:2024-10-10 00:23:32

android java转kotlin + jetpack爬坑<a href=https://www.elefans.com/category/jswz/34/1768372.html style=日记"/>

android java转kotlin + jetpack爬坑日记

前言:刚不久前换了份工作,该公司已有的产品即是使用kotlin (少部分代码是java,比如一些第三方库) +jetpack 的方式来写的,所以赶鸭子上架,需要迅速的熟悉代码并运用

上一家公司的项目,因为年头有些久了,使用的java语言编写的,也就一直没有采用kotlin,也没有使用jetpack,所以本文适合已经有java经验开发android app,现在希望转向kotlin + jetpack的读者

我会将在项目实际过程中遇到的各种问题记录于此,方便大家查阅

在这之前,请先查看该网址(GitHub - MindorksOpenSource/from-java-to-kotlin: From Java To Kotlin - Your Cheat Sheet For Java To Kotlin),了解一些基本的java写法转向kotlin的区别,下面的文章中,如果是该网址有的一些写法,则不再列入

该文章持续更新,凡是遇到从java转换到kotlin的问题,就会记录下来,欢迎关注

1.构造函数

这是大家非常熟悉的自定义控件的构造函数,其他类的构造函数也类似

public class MyViewPager extends ViewPager {public MyViewPager(@NonNull Context context) {super(context);}public MyViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {super(context, attrs);}}

kotlin的写法如下:

class MyViewPager : ViewPager { //kotlin中,继承用://java写法的构造函数,public MyViewPager()在kotlin中,用constructor关键字代替/* 这里我写的时候不是很理解,自己写成了这样* constructor(context : Context){ *    super(context)*  }* 还是习惯了java的写法,用{}来框住方法体,然后在里面写逻辑,但是kotlin这里就直接用:就可以了* 写法就是这样,大家慢慢适应就好了*/constructor(context: Context) : super(context) {}//这里是不是很好奇,为什么AttributeSet后面有个? 在kotlin中,参数后面带个?代表这个参数如果为空的话,并不会抛出空指针异常,同样对应的还有 !!,双感叹号代表这个参数不能为空,如果为空的话,会抛出空指针异常constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {}
}

2.回调函数

写了个测试的类,里面定义了一个接口LayoutChangeListener

public class TestInterface {private LayoutChangeListener listener;interface LayoutChangeListener{void MoveUp();void MoveDown();}public void setListener(LayoutChangeListener listener){this.listener = listener;}
}

java代码调用如下:

TestInterface testInterface = new TestInterface();testInterface.setListener(new TestInterface.LayoutChangeListener() {@Overridepublic void MoveUp() {}@Overridepublic void MoveDown() {}});

kotlin则变成这样:

val testInterface = TestInterface() //这里可以用var,也可以val,val相当于java的 final关键字//object : 简单粗暴的理解为就是java 的 new xxx       testInterface.setListener(object : LayoutChangeListener {override fun MoveUp() {}override fun MoveDown() {}})

3.LiveEventBus

大家以前都是用EventBus,但它可能造成内存泄漏,现在jetpack推出之后,使用LiveEventBus来替代EventBus,它能感知生命周期,使用观察者模式,具体的原理大家自行百度

先简单说明一下LiveEventBus的用法以及和EventBus的区别

//以下是eventbus的使用方法
EventBus.getDefault().register(this)@Subscribe(threadMode = ThreadMode.MAIN)public void onColorEvent(String content){}EventBus.getDefault().post("test")//然后大家会在ondestroy方法里面取消注册
public void onDestroy() {super.onDestroy()EventBus.getDefault().unregister(this)}//使用LiveEventBus的话,就不需要有注册和取消注册这一步了,只需要在application里面初始化的时候,设置自动回收就可以了
LiveEventBus.config().supportBroadcast(this). //是否支持使用广播发送数据,这个就可以跨进程通信啦lifecycleObserverAlwaysActive(true). //设置为true,则整个生命周期都能收到消息,为false,就只有激活状态才能收到,默认是trueautoClear(true); //在没有Observer关联的时候是否自动清除LiveEvent以释放内存//发送方法
LiveEventBus.get("colorEvent").post("test")//接收方法
LiveEventBus.get("colorEvent").observe(this, new Observer<Object>() {@Overridepublic void onChanged(Object o) {}});

那么换成kotlin是这样用的

//发送数据
LiveEventBus.get("colorEvent").post("test")//接收数据
LiveEventBus.get("colorEvent").observe(this, Observer {//是不是有点没看懂?这样怎么接收数据呢,//别急,这里你会看到有个it参数,它就是值,而且是泛型的//比如我们这里发送的是一个string字符串,那么接收就可以这样写val test:String = it.toString()//而如果我们post的不是字符串,是一个实体类怎么办呢?//假设我们发送的是这样://LiveEventBus.get("colorEvent").post(ColorEvent("#ffffff"))//这里ColorEvent的实体类我就不贴代码了,自行脑补//那么接收代码就是这样:val colorEvent:ColorEvent = it as ColorEvent//如果我们一个类里面有多个接收方法,那么就这样接收:if(it is ClassA){ //这里的 it is ClassA 就是java代码的 it instanceof ClassA}else if(it is ClassB){}})

4.Intent跳转Activity

java写法:

Intent intent = new Intent(this,ClassA.class);startActivity(intnet)

kotlin写法:

var intent = Intent(this,ClassA::class.java)startActivity(intent)

5. Handler

java写法:

private Handler mHandler =new Handler(){@Overridepublic void handleMessage(Message msg) {mHandler.sendEmptyMessageDelayed(0,1000);}};

kotlin写法:

private val mHandler = object :Handler(){override fun handleMessage(msg: Message?) {
//这里特别留意,因为java的写法是要用mHandler.出来的
//但如果这里前面写个mHandler.那么会编译不通过sendEmptyMessageDelayed(0,1000)}}

6.jetpack---viewbinding

这绝对是我刚接触jetpack以来认为第一个吊炸天的东西,可以省略非常多的代码。基本再也不用findviewById,也不用BindView(butterknife的用法)了,试想一下:一个复杂的界面,几十个id需要处理,光写findview就花了几分钟,还占用大量篇幅

//在模块的build.gradle文件下加入这行代码即可
//但用这玩意,就意味着你要用kotlin来写代码,是的,google就是这么骚,为了让
//用户转变为用kotlin来写代码,这玩意就只支持kotlin
apply plugin: 'kotlin-android-extensions'
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=""xmlns:app=""xmlns:tools=""android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity"><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="Hello World!"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintRight_toRightOf="parent"app:layout_constraintTop_toTopOf="parent"android:id="@+id/tv_hello"/></androidx.constraintlayout.widget.ConstraintLayout>
//这行代码就是把view都引入啦,只要xml布局有id的,这里都有
import kotlinx.android.synthetic.main.activity_main.* class KtMainActivity : AppCompatActivity(){override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)tv_hello.text = "aaaa" //这里直接使用id的名称就可以了}
}

7. 类引用

java写法

startActivity(new Intent(this,SecondActivity.class))

kotlin写法

startActivity(Intent(this@HelloActivity,SecondActivity::class.java))

8.ExpandListView的convertView引用

java写法

@Overridepublic View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {if(convertView == null){convertView = LayoutInflater.from(context).inflate(R.layout.xxx,parent,false)}return convertView;}

kotlin写法

override fun getGroupView(groupPosition: Int,isExpanded: Boolean,convertView: View?,parent: ViewGroup?): View {var convertView = convertView //这里必须重新赋值,直接用convertView的话,下面的convertView = xxx 就报错了,无法编译if(convertView == null) {convertView = LayoutInflater.from(mContext).inflate(R.layout.adapter_bfb_record_group,null)}return convertView!!}

9. jetpack -- 数据绑定(databinding)

个人认为,数据绑定这玩意的好处,是减少在activity中对于UI数据的操作,当activity比较复杂,上千行代码的时候,用databinding有助于代码简洁

//它是jetpack的组件之一,需要在对应的运行模块的build.gradle文件中加入如下代码
android{dataBinding {enabled = true}
}
//第二步,先写model层(个人习惯,你要先写xml布局也可以)
//接口返回什么,就写什么咯
data class User(val firstname:String)
<!-- 第三步,写xml布局 -->
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android=""xmlns:app=""><data><!-- name自行定义,你想叫什么都行 --><!-- type 就是对应的model层的类名--><variable name="killaxiao" type="com.example.myapplication.User"/></data><androidx.constraintlayout.widget.ConstraintLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"><!-- @{killaxiao.firstname , default=helloWorld} 很好理解吧 --><!-- @{}的语法,上面定义的name.model层的参数,default是默认值 --><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="@{killaxiao.firstname , default=helloWorld}"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintRight_toRightOf="parent"app:layout_constraintTop_toTopOf="parent"android:id="@+id/tv_hello"/></androidx.constraintlayout.widget.ConstraintLayout>
</layout>
override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)//使用databinding之后,就不用使用setcontentview了//这里ActivityMainBinding 是因为我的布局文件叫activity_main,//如果你的布局文件叫activity_test,那么这里就是ActivityTestBindingval binding: ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)//这里在实际使用中,就是从接口获取到值之后,设置进去即可binding.killaxiao = User("bbbbb")}

10.room数据库

这个数据库让我遇到的坑有点多,先说明基础用法

apply plugin: 'kotlin-kapt' //在运行模块的build.gradle目录加入代码
dependencies{implementation "androidx.room:room-runtime:2.3.0"kapt "androidx.room:room-compiler:2.3.0" //我是用kotlin开发,所以前缀是kapt,如果是java的,请用annotationProcessor,但本文是介绍kotlin的,所以不做java的讲解
}

引入写完了之后,第一步,先来创建一个表

@Entity(tableName = "users") //这里如果不写tableName的话,那么表名就是类名(UserTable)
data class UserTable(//下面这些应该不用过多解释了,userid主键自增,其他的name = "xxx"就是列名@PrimaryKey(autoGenerate = true) var userid:Int,@ColumnInfo(name = "username") var username:String,@ColumnInfo(name = "password") var password:String,@ColumnInfo(name = "permission") var permission:String,@ColumnInfo(name = "phone") var phone:String,@ColumnInfo(name = "company") var company:String
)

然后写它的增删改查方法

@Dao
interface UserDao { //必须是接口@Query("select * from users where userid = :id") //方法中的参数,用:id来获取fun getUserById(id:Int):UserBean@Query("select * from users where username = :name")fun getUserByName(name:Int):UserBean@Query("select count(*) from users where username = :username and password = :password")fun Login(username:String,password:String):Int@Insert(onConflict = OnConflictStrategy.REPLACE)fun insertUser(user:UserTable)@Updatefun updateUser(user:UserTable)@Query("update users set username = :username and password = :password and phone = :phone and company = :company where userid = :id")fun updateUserById(id:Int,username:String,password:String,phone:String,company:String)
}

接下来创建数据库

//entities里面,有几个表,就写几个创建表对应的类名,version版本号不用说,最后一个参数exportSchema可以不填,默认是true
@Database(entities = [UserTable::class,MessageTable::class],version = 1,exportSchema = true)
abstract class AppDatabase :RoomDatabase(){abstract fun userDao():UserDaoabstract fun messageDao():MessageDaocompanion object{private var instance:AppDatabase? = nullfun getInstance(context: Context):AppDatabase{if(instance == null){//allowMainThreadQueries是允许在主线程执行instance = Room.databaseBuilder(context.applicationContext,AppDatabase::class.java,"xingtong").allowMainThreadQueries().build()}return instance as AppDatabase}}}
//启动页copy了以前项目的,所以是java写的,大家可以自己写kotlin的代码
AppDatabase database = AppDatabase.Companion.getInstance(WelcomeActivity.this);try {UserDao userDao = AppDatabase.Companion.getInstance(WelcomeActivity.this).userDao();//这里是自己写的工具类,读取assets目录下的default_user.json,代码就不贴了JSONObject jsonObject = new JSONObject(AssetsReader.getJson("default_user.json", WelcomeActivity.this));JSONArray jsonArray = jsonObject.optJSONArray("data");for(int i=0;i<jsonArray.length();i++){JSONObject obj = jsonArray.optJSONObject(i);UserTable userTable = new UserTable(-1,obj.optString("username"),obj.optString("password"),obj.optString("permission"),obj.optString("phone"),obj.optString("company"));userDao.insertUser(userTable);}startActivity(new Intent(WelcomeActivity.this, LoginActivity.class));finish();}catch (Exception e){e.printStackTrace();}

NOTE:接下来说坑

1.编译时报错

Schema export directory is not provided to the annotation processor so we cannot export the schema. You can either provide `room.schemaLocation` annotation processor argument OR set exportSchema to false.

这个要在刚才的运行模块build.gradle里面加入代码,或者创建数据库的代码那里 设置 exportSchema =false,不过推荐下面这种做法

defaultConfig {applicationId "xxx"minSdkVersion 23targetSdkVersion 28versionCode 1versionName "1.0"testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"//加入以下代码javaCompileOptions {annotationProcessorOptions {arguments = ["room.schemaLocation":"$projectDir/schemas".toString()]}}}

2. 接收不到回调

AppDatabase.Companion.getInstance(WelcomeActivity.this,new RoomDatabase.Callback(){@Overridepublic void onCreate(@NonNull SupportSQLiteDatabase db) {try {UserDao userDao = AppDatabase.Companion.getInstance(WelcomeActivity.this).userDao();JSONObject jsonObject = new JSONObject(AssetsReader.getJson("default_user.json", WelcomeActivity.this));JSONArray jsonArray = jsonObject.optJSONArray("data");for(int i=0;i<jsonArray.length();i++){JSONObject obj = jsonArray.optJSONObject(i);UserTable userTable = new UserTable(-1,obj.optString("username"),obj.optString("password"),obj.optString("permission"),obj.optString("phone"),obj.optString("company"));userDao.insertUser(userTable);}startActivity(new Intent(WelcomeActivity.this, LoginActivity.class));finish();}catch (Exception e){e.printStackTrace();}}});

在创建数据库的时候,获取回调,是没有响应的,直到有对数据库的具体操作,才会有回调,所以这个回调有点鸡肋,大可不用

3. 上述代码引发的崩溃

java.lang.IllegalStateException: getDatabase called recursively

这是因为AppDatabase.Companion.getInstance得到了一个数据库的对象引用,然后我在getInstance这个方法里面写了

instance!!.beginTransaction() 这行代码,即可收到onCreate回调,然后在回调这里再获取引用,在userDao.insertUser的时候报错了,在获取getInstance的时候,数据库引用已经锁了

所以我去掉了getInstance方法里面的instanse!!.beginTransaction(),改成如下代码,即可正常运行

        AppDatabase database = AppDatabase.Companion.getInstance(WelcomeActivity.this);try {UserDao userDao = database.userDao();JSONObject jsonObject = new JSONObject(AssetsReader.getJson("default_user.json", WelcomeActivity.this));JSONArray jsonArray = jsonObject.optJSONArray("data");for(int i=0;i<jsonArray.length();i++){JSONObject obj = jsonArray.optJSONObject(i);UserTable userTable = new UserTable(-1,obj.optString("username"),obj.optString("password"),obj.optString("permission"),obj.optString("phone"),obj.optString("company"));userDao.insertUser(userTable);}startActivity(new Intent(WelcomeActivity.this, LoginActivity.class));finish();}catch (Exception e){e.printStackTrace();}

11.kotlin协程

协程的概念在kotlin1.3的版本提出来,所以如果要使用协程的话,依赖的版本要高于1.3才行。至于协程是啥?笔者担心解释不清楚,建议大家还是去看官方的描述,可以简单的认为,协程,就是可以让你用同步化的逻辑去执行异步的代码,可以替代一些回调函数。

笔者在写文章的时候,官方的协程版本是1.3.9

//在项目执行模块的build.gradle配置文件中加入kotlin协程implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9"implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9"

启动协程的几种方式:

1.runBlocking{},这种方式启动的协程会阻塞当前线程,直至runBlocking里面的逻辑执行结束,实际使用中,应避免使用该方式启动协程,常用于测试协程代码用

        var a = 0runBlocking {//创建一个协程,但这个协程是会阻塞当前线程的,直至协程的逻辑运行完毕a += 1}Log.e("test","a的值:$a") //因为runBlocking会阻塞线程,所以这里a输出的值是1

如果改成这样,那么程序就会卡死

        var a = 0runBlocking {//创建一个协程,但这个协程是会阻塞当前线程的,直至协程的逻辑运行完毕while (true) {a += 1Log.e("test","a的值:$a") }}

2.GlobalScope.launch{},这种方式启动的协程不会阻塞当前线程,但它的生命周期是等同于当前应用的生命周期,即假设这个协程在Test1Activity中启动,然后Test1Activity跳转至Test2Activity,并finish掉了Test1Activity,但协程内的代码依然还是在执行的,实际使用中,可以替代部分后台service执行的代码逻辑,而其它的情况则应该要避免使用这种方式启动协程

        GlobalScope.launch {//这里也是创建一个协程,但这个协程的生命周期是整个应用程序的生命周期,也就是说,即使这个activity销毁了,协程也还在执行while (true){ //因为这种启动方式不会阻塞线程,所以即使一直执行,也不会卡死界面test_result+=1Log.e("test","test_result的值:$test_result")}}btn_click.setOnClickListener {//点击之后跳转到Test2Activity,并finish掉当前activity,但因为上方协程生命周期是跟随应用程序的生命周期,不会在activity关闭后停止运行,所以即使到了Test2Activity,也还是一直在输入test_result的值startActivity(Intent(this,Test2Activity::class.java))finish()}

3.CoroutineScope + launch{}/async{},这种方式启动的协程不会阻塞线程,并且会跟随当前生命周期的结束而结束,所以实际使用中,应尽量使用这种方式启动协程

3.1 在activity中实现协程

class MainActivity : BaseActivity(),CoroutineScope {override val coroutineContext: CoroutineContextget() = Dispatchers.Main //会阻塞主线程,官方描述是这种调度器可在主线程执行协程,只能用于界面交互和执行快速工作//get() = Dispatchers.IO //不会阻塞主线程,官方描述是用于读写文件,访问网络用的//get() = Dispatchers.Default //不会阻塞主线程,最大并发数是CPU的核心数,默认2,官方描述是用于占用大量CPU资源的工作,比如对列表排序或者解析jsonprivate lateinit var test1_job:Jobprivate lateinit var test2_job:Joboverride fun initView() {test1_job=launch {while (true) {if(test1_job.isActive) { //如果不判断协程是否在取消状态,那么即使调用test1_job.cancel,协程也不会停止运行test_result += 1Log.e("test", "test_result的值:$test_result")}}}test2_job=launch {var async1 = async {delay(3000) //这里特意延迟3秒,再返回5 所以这里设想一下,是网络请求5}var async2 = async {delay(5000) //这里延迟5秒,再返回10 ,如果需要同步上面网络请求的结果一起做UI更新的话,async就派上了用场10}//为了展示async的用法,所以上面创建了两个async的协程,分别延迟3秒和5秒,但因为async会同步执行,await()会等待执行结果,所以这里是会延迟5秒之后,得到//async1和async2的值,才输出日志Log.e("test","async1+async2=${async1.await()+async2.await()}") //输出的结果是15}}override fun onDestroy() {super.onDestroy()test1_job.cancel() //不写取消方法,不会跟随activity的销毁而销毁test2_job.cancel()}
}

3.2 直接创建一个协程作用域

var scope =CoroutineScope(Dispatchers.IO)var test3_job = scope.launch {while (true) {test_result += 1Log.e("test", "test_result的值:$test_result")}}

12.创建方法

其实这个很简单,但为什么要写呢,是因为我在用了kotlin几个月之后,才留意到原来也可以这么写

val sumLanda:(Int,Int)->Int = {x,y ->x+y} //这个是有返回参数的,和下方写法完全一样fun sumLanda2(x:Int,y:Int):Int{return x+y}
//但编辑器上能看得出来两种写法调用的区别,调用sumLanda是和参数调用一样的,是紫色的,但是调用sumLanda2是白色的
Log.e("test",sumLanda(2,6).toString())
Log.e("test",sumLanda2(2,6).toString())//如果不需要返回参数,就直接这么写就OK了
val sumLanda3:(x:Int,y:Int)->Unit = {it1,it2-> //有多个参数,这里必须声明,因为这种属于是lambda表达式Log.e("test",(it1+it2).toString())}val sumLanda4:(x:Int)->Unit ={ //只有一个参数,就可以不声明,默认是itLog.e("test",it.toString())}

13.类型检测

咱们写java的朋友都知道,有些时候我们写了一些父类,用的T类型,然后到具体的子类使用的时候需要进行类型检测,于是使用 instanceof 关键字,那么在kotlin里面使用的是 is 关键字,不过kotlin还提供了 !is 方法,就是除开某某类型的意思,看代码

fun isTest(obj:Any):Int?{if(obj !is String){return obj.toString().length}return null}Log.e("test","isTest:"+isTest(3)) //输出isTest:1
Log.e("test","isTest:"+isTest("12345")) //输出isTest:nullfun isTest2(obj:Any):Int?{if(obj is String){return obj.toString().length}return null}Log.e("test","isTest2:"+isTest2(3)) //输出isTest2:null
Log.e("test","isTest2:"+isTest2("12345")) //输出isTest2:5

更多推荐

android java转kotlin + jetpack爬坑日记

本文发布于:2024-02-13 22:02:21,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1761023.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:日记   java   android   jetpack   kotlin

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!