赞
踩
前言:刚不久前换了份工作,该公司已有的产品即是使用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() {
- @Override
- public void MoveUp() {
-
- }
-
- @Override
- public 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,就只有激活状态才能收到,默认是true
- autoClear(true); //在没有Observer关联的时候是否自动清除LiveEvent以释放内存
-
- //发送方法
- LiveEventBus.get("colorEvent").post("test")
-
- //接收方法
- LiveEventBus.get("colorEvent").observe(this, new Observer<Object>() {
- @Override
- public 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(){
- @Override
- public 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="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- tools:context=".MainActivity">
-
- <TextView
- android: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写法
- @Override
- public 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="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- >
- <data>
- <!-- name自行定义,你想叫什么都行 -->
- <!-- type 就是对应的model层的类名-->
- <variable name="killaxiao" type="com.example.myapplication.User"/>
- </data>
-
- <androidx.constraintlayout.widget.ConstraintLayout
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <!-- @{killaxiao.firstname , default=helloWorld} 很好理解吧 -->
- <!-- @{}的语法,上面定义的name.model层的参数,default是默认值 -->
- <TextView
- android: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,那么这里就是ActivityTestBinding
- val 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)
-
- @Update
- fun 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():UserDao
- abstract fun messageDao():MessageDao
-
- companion object{
- private var instance:AppDatabase? = null
- fun 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();
- }
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 23
- targetSdkVersion 28
- versionCode 1
- versionName "1.0"
-
- testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
- //加入以下代码
- javaCompileOptions {
- annotationProcessorOptions {
- arguments = ["room.schemaLocation":
- "$projectDir/schemas".toString()]
- }
- }
- }
2. 接收不到回调
- AppDatabase.Companion.getInstance(WelcomeActivity.this,new RoomDatabase.Callback(){
- @Override
- public 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 = 0
- runBlocking {//创建一个协程,但这个协程是会阻塞当前线程的,直至协程的逻辑运行完毕
- a += 1
- }
- Log.e("test","a的值:$a") //因为runBlocking会阻塞线程,所以这里a输出的值是1
如果改成这样,那么程序就会卡死
- var a = 0
- runBlocking {//创建一个协程,但这个协程是会阻塞当前线程的,直至协程的逻辑运行完毕
- while (true) {
- a += 1
- Log.e("test","a的值:$a")
- }
- }
2.GlobalScope.launch{},这种方式启动的协程不会阻塞当前线程,但它的生命周期是等同于当前应用的生命周期,即假设这个协程在Test1Activity中启动,然后Test1Activity跳转至Test2Activity,并finish掉了Test1Activity,但协程内的代码依然还是在执行的,实际使用中,可以替代部分后台service执行的代码逻辑,而其它的情况则应该要避免使用这种方式启动协程
- GlobalScope.launch {//这里也是创建一个协程,但这个协程的生命周期是整个应用程序的生命周期,也就是说,即使这个activity销毁了,协程也还在执行
- while (true){ //因为这种启动方式不会阻塞线程,所以即使一直执行,也不会卡死界面
- test_result+=1
- Log.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: CoroutineContext
- get() = Dispatchers.Main //会阻塞主线程,官方描述是这种调度器可在主线程执行协程,只能用于界面交互和执行快速工作
- //get() = Dispatchers.IO //不会阻塞主线程,官方描述是用于读写文件,访问网络用的
- //get() = Dispatchers.Default //不会阻塞主线程,最大并发数是CPU的核心数,默认2,官方描述是用于占用大量CPU资源的工作,比如对列表排序或者解析json
-
- private lateinit var test1_job:Job
- private lateinit var test2_job:Job
-
- override fun initView() {
- test1_job=launch {
- while (true) {
- if(test1_job.isActive) { //如果不判断协程是否在取消状态,那么即使调用test1_job.cancel,协程也不会停止运行
- test_result += 1
- Log.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 += 1
- Log.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 ={ //只有一个参数,就可以不声明,默认是it
- Log.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:null
-
- fun 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
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。