UI开发的点滴

UI开发

基础知识:

  1. dp,这是一种屏幕密度无关的尺寸单位,可以保证在不同分辨率的手机上显示效果尽可能地一致。
  2. 文字大小要使用sp作为单位,这样当用户在系统中修改了文字显示尺寸时,应用程序中的文字大小也会跟着变化;
  3. Android控件的可见属性,可以通过android:visibility进行指定,可选值有3种:visible、invisible和gone。
    1. visible表示控件是可见的,这个值是默认值,不指定android:visibility时,控件都是可见的;
    2. invisible表示控件不可见,但是它仍然占据着原来的位置和大小,可以理解成控件变成透明状态了;
    3. gone则表示控件不仅不可见,而且不再占用任何屏幕空间。

控件

基础知识:

  1. android:gravity来指定文字的对齐方式,可选值有top、bottom、start、end、center等,可以用“|”来同时指定多个值;

TextView

无赘述。

Button

Android系统默认会将按钮上的英文字母全部转换成大写,可能是认为按钮上的内容都比较重要吧。如果这不是你想要的效果,可以在XML中添加android:textAllCaps=”false” 这个属性。

注册监听器的两种方式:

  1. Java函数式API;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class MainActivity : AppCompatActivity() { 
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    button.setOnClickListener {
    // 在此处添加逻辑
    }
    }
    }
  2. 使用实现接口的方式来进行注册。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class MainActivity : AppCompatActivity(), View.OnClickListener { 

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    button.setOnClickListener(this)
    }

    override fun onClick(v: View?) {
    when (v?.id) {
    R.id.button -> {
    // 在此处添加逻辑
    }
    }
    }
    }

    这里我们让MainActivity实现了View.OnClickListener接口,并重写了onClick()方法,然后在调用button的setOnClickListener()方法时将MainActivity的实例传了进去。这样每当点击按钮时,就会执行onClick()方法中的代码了。

EditText

  1. 可以使用android:hint属性指定一段提示性的文本;
  2. 可以使用android:maxLines指定最大行数。

ImageView

现在最主流的手机屏幕分辨率大多是xxhdpi的,需要在res目录下新建一个drawable-xxhdpi目录。

ProgressBar

  1. android:max属性给进度条设置一个最大值;
  2. style属性可以指定成进度条样式。

AlertDialog

AlertDialog可以在当前界面弹出一个对话框。调用setPositiveButton()方法为对话框设置确定按钮的点击事件,调用setNegativeButton()方法设置取消按钮的点击事件。

布局

LinearLayout

LinearLayout又称作线性布局。

  1. 如果LinearLayout的排列方向是horizontal,内部的控件就绝对不能将宽度指定为match_parent;如果LinearLayout的排列方向是vertical,内部的控件就不能将高度指定为match_parent。
  2. 当LinearLayout的排列方向是horizontal时,只有垂直方向上的对齐方式才会生效。当LinearLayout的排列方向是vertical时,只有水平方向上的对齐方式才会生效。

android:layout_weight这个属性允许我们使用比例的方式来指定控件的大小,它在手机屏幕的适配性方面可以起到非常重要的作用,本质是按照数值和进行归一化分配比例。如果是水平布局,使用android:layout_weight后,控件的宽度就不应该再由android:layout_width来决定了,直接赋值0dp即可;垂直布局同理。

RelativeLayout

RelativeLayout又称作相对布局。每个控件都是相对于父布局进行定位的,也可以相对于控件进行定位。

一个有两组属性,一组按照边缘定位(如:android:layout_alignLeft),一组按照中心(如: android:layout_toLeftOf)定位。

当一个控件去引用另一个控件的id时,该控件一定要定义在引用控件的后面,不然会出现找不到id的情况。

FrameLayout

FrameLayout又称作帧布局,简单且应用场景少。以使用layout_gravity属性来指定控件在布局中的对齐方式。

自定义控件

所用的所有控件都是直接或间接继承自View的,所用的所有布局都是直接或间接继承自ViewGroup的。View是Android中最基本的一种UI组件,它可以在屏幕上绘制一块矩形区域,并能响应这块区域的各种事件,因此,各种控件其实就是在View的基础上又添加了各自特有的功能。而ViewGroup则是一种特殊的View,它可以包含很多子View和子ViewGroup,是一个用于放置控件和布局的容器。

引入布局

如果在每个Activity的布局中都编写一遍同样的布局代码,明显就会导致代码的大量重复。这时我们就可以使用引入布局的方式来解决这个问题。

只需要通过一行include语句引入布局。

1
2
3
4
5
6
7
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
android:layout_width="match_parent"
android:layout_height="match_parent" >

<include layout="@layout/title" />

</LinearLayout>

创建自定义控件

这里我们在TitleLayout的主构造函数中声明了Context(本质就是Activity的实例)和AttributeSet这两个参数,在布局中引入TitleLayout控件时就会调用这个构造函数。通过LayoutInflater的from()方法可以构建出一个LayoutInflater对象,然后调用inflate()方法就可以动态加载一个布局文件。

1
2
3
4
5
6
7
class TitleLayout(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) { 

init {
LayoutInflater.from(context).inflate(R.layout.title, this)
}

}

添加自定义控件和添加普通控件的方式基本是一样的,只不过在添加自定义控件的时候,我们需要指明控件的完整类名,包名在这里是不可以省略的。

1
2
3
4
5
6
7
8
9
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
android:layout_width="match_parent"
android:layout_height="match_parent" >

<com.example.uicustomviews.TitleLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" />

</LinearLayout>

注意,TitleLayout中接收的context参数实际上是一个Activity的实例,在返回按钮的点击事件里,我们要先将它转换成Activity类型,然后再调用finish()方法销毁当前的Activity。Kotlin中的类型强制转换使用的关键字是as

1
2
3
4
5
6
7
8
9
10
11
12
13
class TitleLayout(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) { 

init {
LayoutInflater.from(context).inflate(R.layout.title, this)
titleBack.setOnClickListener {
val activity = context as Activity
activity.finish()
}
titleEdit.setOnClickListener {
Toast.makeText(context, "You clicked Edit button", Toast.LENGTH_SHORT).show()
}
}
}

ListView

简单用法

数据是无法直接传递给ListView的,需要借助适配器来完成。Android中提供了很多适配器的实现类,如ArrayAdapter。它可以通过泛型来指定要适配的数据类型,然后在构造函数中把要适配的数据传入。在ArrayAdapter的构造函数中依次传入Activity的实例、ListView子项布局的id,以及数据源。

还需要调用ListView的setAdapter()方法,将构建好的适配器对象传递进去,这样ListView和数据之间的关联就建立完成。

定制界面

创建一个自定义的适配器,这个适配器继承自ArrayAdapter,并将泛型指定为数据类。定义一个主构造函数,用于将Activity的实例、ListView子项布局的id和数据源传递进来(类似标准ArrayAdapter)。另外又重写了getView()方法,这个方法在每个子项被滚动到屏幕内的时候会被调用。

接着在onCreate()方法中创建 了适配器对象,并将它传递给ListView,这样定制ListView界面的任务就完成了。

提升运行效率

  1. getView()方法中还有一个convertView参数,这个参数用于将之前加载好的布局进行缓存,以便之后进行重用,我们可以借助这个参数来进行性能优化,避免重复加载布局。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class FruitAdapter(activity: Activity, val resourceId: Int, data: List<Fruit>) : 
    ArrayAdapter<Fruit>(activity, resourceId, data) {
    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
    val view: View
    if (convertView == null) {
    view = LayoutInflater.from(context).inflate(resourceId, parent, false)
    } else {
    view = convertView
    }
    val fruitImage: ImageView = view.findViewById(R.id.fruitImage)
    val fruitName: TextView = view.findViewById(R.id.fruitName)
    val fruit = getItem(position) // 获取当前项的Fruit实例
    if (fruit != null) {
    fruitImage.setImageResource(fruit.imageId)
    fruitName.text = fruit.name
    }
    return view
    }
    }
  2. 借助ViewHolder来对性能进行优化,避免重复调用View的findViewById()方法来获取一次控件的实例。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    class FruitAdapter(activity: Activity, val resourceId: Int, data: List<Fruit>) : 
    ArrayAdapter<Fruit>(activity, resourceId, data) {
    inner class ViewHolder(val fruitImage: ImageView, val fruitName: TextView)
    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
    val view: View
    val viewHolder: ViewHolder
    if (convertView == null) {
    view = LayoutInflater.from(context).inflate(resourceId, parent, false)
    val fruitImage: ImageView = view.findViewById(R.id.fruitImage)
    val fruitName: TextView = view.findViewById(R.id.fruitName)
    viewHolder = ViewHolder(fruitImage, fruitName)
    view.tag = viewHolder
    } else {
    view = convertView
    viewHolder = view.tag as ViewHolder
    }

    val fruit = getItem(position) // 获取当前项的Fruit实例
    if (fruit != null) {
    viewHolder.fruitImage.setImageResource(fruit.imageId)
    viewHolder.fruitName.text = fruit.name
    }
    return view
    }

    }

    新增一个内部类ViewHolder,用于对ImageView和TextView的控件实例进行缓存。然后调用View的setTag()方法,将ViewHolder对象存储在View中。

点击事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MainActivity : AppCompatActivity() { 

private val fruitList = ArrayList<Fruit>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initFruits() // 初始化水果数据
val adapter = FruitAdapter(this, R.layout.fruit_item, fruitList)
listView.adapter = adapter
listView.setOnItemClickListener { _, _, position, _ ->
val fruit = fruitList[position]
Toast.makeText(this, fruit.name, Toast.LENGTH_SHORT).show()
}
}

...

}

使用setOnItemClickListener()方法为ListView注册了一个监听器,这是一个Java单抽象方法接口,故使用Java函数式API编程。Kotlin允许我们将没有用到的参数使用下划线来替代。

RecyclerView

基本用法

RecyclerView是一个增强版的 ListView。RecyclerView属于新增控件,考虑到兼容性,Google将RecyclerView控件定义在了AndroidX 当中,我们需要在项目的build.gradle中添加RecyclerView库的依赖。

1
2
3
4
5
6
7
8
9
10
11
dependencies { 
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.core:core-ktx:1.0.2'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.recyclerview:recyclerview:1.0.0'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
}

为RecyclerView准备一个适配器,新建FruitAdapter类,让这个适配器继承自RecyclerView.Adapter,并将泛型指定为FruitAdapter.ViewHolder。其中,ViewHolder是我们在FruitAdapter中定义的一个内部类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class FruitAdapter(val fruitList: List<Fruit>) : 
RecyclerView.Adapter<FruitAdapter.ViewHolder>() {

inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val fruitImage: ImageView = view.findViewById(R.id.fruitImage)
val fruitName: TextView = view.findViewById(R.id.fruitName)
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.fruit_item, parent, false)
return ViewHolder(view)
}

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val fruit = fruitList[position]
holder.fruitImage.setImageResource(fruit.imageId)
holder.fruitName.text = fruit.name
}

override fun getItemCount() = fruitList.size

}

由于FruitAdapter是继承自RecyclerView.Adapter的,那么就必须重写onCreateViewHolder()、onBindViewHolder()和getItemCount()这3个方法。onCreateViewHolder()方法是用于创建ViewHolder实例;onBindViewHolder()方法用于对RecyclerView子项的数据进行赋值,会在每个子项被滚动到屏幕内的时候执行;getItemCount()方法就非常简单了,它用于告诉RecyclerView一共有多少子项,直接返回数据源的长度就可以了。

在onCreate()方法中先创建了一个LinearLayoutManager对象,并将它设置到RecyclerView当中。最后调用 RecyclerView的setAdapter()方法来完成适配器设置,这样RecyclerView和数据之间的关联就建立完成。

实现横向滚动

MainActivity中只加入了一行代码,调用LinearLayoutManager的setOrientation()方法设置布局的排列方向。默认是纵向排列的。

其他布局

ListView的布局排列是由自身去管理的,而RecyclerView则将这个工作交给了LayoutManager。LayoutManager制定了一套可扩展的布局排列接口,子类只要按照接口的规范来实现,就能定制出各种不同排列方式的布局了。

不赘述。

点击事件

RecyclerView并没有提供类似于setOnItemClickListener()这样的注册监听器方法。RecyclerView直接摒弃了子项点击事件的监听器,让所有的点击事件都由具体的View去注册,在onCreateViewHolder()方法中注册点击事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class FruitAdapter(val fruitList: List<Fruit>) : 
RecyclerView.Adapter<FruitAdapter.ViewHolder>() {
...
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.fruit_item, parent, false)
val viewHolder = ViewHolder(view)
viewHolder.itemView.setOnClickListener {
val position = viewHolder.adapterPosition
val fruit = fruitList[position]
Toast.makeText(parent.context, "you clicked view ${fruit.name}",
Toast.LENGTH_SHORT).show()
}
viewHolder.fruitImage.setOnClickListener {
val position = viewHolder.adapterPosition
val fruit = fruitList[position]
Toast.makeText(parent.context, "you clicked image ${fruit.name}",
Toast.LENGTH_SHORT).show()
}
return viewHolder
}
...
}

编写页面

9-Patch图片

它是一种被特殊处理过的png图片,能够指定哪些区域可以被拉伸、哪些区域不可以。

制作过程不赘述。

要将原来的.png图片删除,只保留制作好的.9.png图片。Android项目中不允许同一文件夹下有两张相同名称的图片(即使后缀名不同也不行)。

聊天界面

定义常量的关键字是const,注意只有在单例类、companion object或顶层方法中才可以使用const关键字。

根据不同的viewType创建不同的界面。要重写 getItemViewType()方法,并在这个方法中返回当前position对应的消息类型。

使用适配器的notifyItemInserted()方法,用于通知列表有新的数据插入,这样新增的一条消息才能够在RecyclerView中显示出来。或者调用适配器的notifyDataSetChanged()方法,它会将RecyclerView中所有可见的元素全部刷新,这样不管是新增、删除、还是修改元素,界面上都会显示最新的数据,但缺点是效率会相对差一些。

调用RecyclerView的scrollToPosition()方法将显示的数据定位到最后一行,以保证一定可以看得到最后发出的一条消息。最后调用EditText的setText()方法将输入的内容清空。

具体代码不赘述。

Kotlin课堂

主要讲解延迟初始化密封类

延迟初始化

延迟初始化使用的是lateinit关键字,它可以告诉Kotlin编译器,我会在晚些时候对这个变量进行初始化,这样就不用在一开始的时候将它赋值为null了,同时类型声明也不用添加?,也不再需要进行判空处理。

使用lateinit的风险:变量还没有初始化的情况下就直接使用它,那么程序就一定会崩溃,并且抛出一个UninitializedPropertyAccessException异常。

::adapter.isInitialized可用于判断adapter变量是否已经初始化,这是固定写法。

密封类

由于Kotlin的语法规定,when语句必须要以else结束,否则编译器会认为这里缺少条件分支,产生代码冗余;同时写else条件还有一个潜在的风险,如果我们现在新增了一个Unknown类并实现接口,用于表示未知的执行结果,但是忘记在方法中添加相应的条件分支,编译器在这种情况下是不会提醒我们的,而是会在运行的时候进入else条件里面,从而抛出异常并导致程序崩溃。

密封类的关键字是sealed class,密封类是一个可继承的类,因此在继承它的时候需要在后面加上一对括号。

当在when语句中传入一个密封类变量作为条件时,Kotlin编译器会自动检查该密封类有哪些子类,并强制要求你将每一个子类所对应的条件全部处理。

另外再多说一句,密封类及其所有子类只能定义在同一个文件的顶层位置,不能嵌套在其他类中,这是被密封类底层的实现机制所限制的。

探究Activity

Activity 的基本用法

创建

新建Activity 对话框

  • Generate Layout File表示会自动为FirstActivity创建一个对应的布局文件;
  • Launcher Activity表示会自动将Activity设置为当前项目的主Activity;
  • 勾选Backwards Compatibility表示会为项目启用向下兼容旧版系统的模式,一般勾选。

创建和加载布局

  • 如果你需要在XML中引用一个id,就使用 @id/id_name这种语法,而如果你需要在XML中定义一个id,则要使用 @+id/id_name这种语法;

  • match_parent表示让当前元素和父元素一样宽;

  • wrap_content表示当前元素的高度只要能刚好包含里面的内容;

  • setContentView() 方法来给当前的Activity加载一个布局,一般传入一个布局文件的id,项目中添加的任何资源都会在R文件中生成一个相应的资源id,因此我们刚才创建的xml布局的id现在已经添加到R文件中了,引用即可;

  • 所有的Activity都要在AndroidManifest.xml中进行注册才能生效,Activity的注册声明要放在<application>标签内,这里是通过标签<application>来对Activity进行注册的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <manifest xmlns:android="http://schemas.android.com/apk/res/android" 
    package="com.example.activitytest">
    <application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity android:name=".FirstActivity">
    </activity>
    </application>
    </manifest>

    在<activity>标签中,我们使用了android:name来指定具体注册哪一个Activity,那么这里填入的.FirstActivity是com.example.activitytest.FirstActivity的缩写而已。由于在最外层的标签中已经通过package属性指定了程序的包名是com.example.activitytest,因此在注册Activity时,这一部分可以省略;

  • 配置主Activity在<activity>标签的内部加入<intent-filter>标签,并在这个标签里添加两句声明即可

    1
    2
    3
    4
    <intent-filter> 
    <action android:name="android.intent.action.MAIN" />
    <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>

使用Toast

Toast是Android系统提供的一种非常好的提醒方式,在程序中可以使用它将一些短小的信息通知给用户,这些信息会在一段时间后自动消失,并且不会占用任何屏幕空间。

1
2
3
4
5
6
7
8
override fun onCreate(savedInstanceState: Bundle?) { 
super.onCreate(savedInstanceState)
setContentView(R.layout.first_layout)
val button1: Button = findViewById(R.id.button1)
button1.setOnClickListener {
Toast.makeText(this, "You clicked Button 1", Toast.LENGTH_SHORT).show()
}
}

通过findViewById() 方法获取在布局文件中定义的元素,这里我们传入id来得到按钮的实例。findViewById()方法返回的是一个继承自View的泛型对象,因此Kotlin无法自动推导出它是一个Button还是其他控件,所以我们需要显式地声明成类型。

注意,原书中使用kotlin-android-extensions插件自动获取一个具有相同名称的变量的办法已经行不通,谷歌官方已经舍弃该插件,使用其他方法进行替代。

使用Menu

  • 老版本IDE可以尝试按照原书中的教程,新版IDE应该直接创建Layout xml file,然后根改为menu。

  • 重写onCreateOptionsMenu()方法:

    1
    2
    3
    4
    override fun onCreateOptionsMenu(menu: Menu?): Boolean { 
    menuInflater.inflate(R.menu.main, menu)
    return true
    }

    这里是Java Bean的概念,它是一个非常简单的Java类,会根据类中的字段自动生成相应的Getter和Setter方法。在Kotlin中调用这种语法结构的Java方法时,可直接对字段进行了赋值和读取。其实这就是Kotlin给我们提供的语法糖,它会在背后自动将上述代码转换成调用setPages()方法和getPages()方法。

    menuInflater就使用了这种语法糖,它实际上是调用了父类的getMenuInflater() 方法。getMenuInflater()方法能够得到一个MenuInflater对象,再调用它的inflate()方法,就可以给当前Activity创建菜单了。inflate() 方法接收两个参数:第一个参数用于指定我们通过哪一个资源文件来创建菜单,这里当然是传入R.menu.main;第二个参数用于指定我们的菜单项将添加到哪一个Menu对象当中,这里直接使用onCreateOptionsMenu()方法中传入的menu参数。最后给这个方法返回true,表示允许创建的菜单显示出来,如果返回了false,创建的菜单将无法显示。

    注意:menuInflater是Context的一个成员变量,getMenuInflater()是Context的一个方法,Activity继承自Context。

销毁

Activity类提供了一个finish() 方法,我们只需要调用一下这个方法就可以销毁当前的Activity。

1
2
3
button1.setOnClickListener { 
finish()
}

使用Intent

Intent用于跳转不同的Activity。

使用显式Intent

Intent有多个构造函数的重载,其中一个是Intent(Context packageContext, Class cls) 。这个构造函数接收两个参数:第一个参数Context要求提供一个启动Activity的上下文;第二个参数Class用于指定想要启动的目标Activity,通过这个构造函数就可以构建出Intent的“意图”。Activity类中提供了一个startActivity()方法,专门用于启动Activity,它接收一个Intent参数,这里我们将构建好的Intent传入startActivity()方法就可以启动目标Activity了。

1
2
3
4
button1.setOnClickListener { 
val intent = Intent(this, SecondActivity::class.java)
startActivity(intent)
}

注意,Kotlin中SecondActivity::class.java的写法就相当于Java中SecondActivity.class的写法

使用隐式Intent

隐式Intent不明确指出想要启动哪一个Activity,而是指定了一系列更为抽象的action和category等信息,然后交由系统去分析这个Intent,并帮我们找出合适的Activity去启动。

通过在<activity>标签下配置<intent-filter>的内容,可以指定当前Activity能够响应的action和category,在AndroidManifest.xml中进行配置:

1
2
3
4
5
6
<activity android:name=".SecondActivity" > 
<intent-filter>
<action android:name="com.example.activitytest.ACTION_START" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>

在<action>标签中我们指明了当前Activity可以响应 com.example.activitytest.ACTION_START这个action,而<category>标签则包含了一些附加信息,更精确地指明了当前Activity能够响应的Intent中还可能带有的category。只有<action>和<category>中的内容同时匹配Intent中指定的action和category时,这个Activity才能响应该Intent。

  1. 最新版中,应将组件导出项android:exported设置为true,否则隐式调用会报错;
  2. <category>中必须有DEFAULT,因为无论啥,底层会自动添加DEFAULT策略,没有会导致程序崩溃。

更多隐式Intent 的用法

  1. 网页

    1
    2
    3
    4
    5
    button1.setOnClickListener { 
    val intent = Intent(Intent.ACTION_VIEW)
    intent.data = Uri.parse("https://www.baidu.com")
    startActivity(intent)
    }

    这里我们首先指定了Intent的action是Intent.ACTION_VIEW,这是一个Android系统内置的动作,其常量值为android.intent.action.VIEW。然后通过Uri.parse()方法将一个网址字符串解析成一个Uri对象,再调用Intent的setData()方法将这个Uri对象传递进去。

  2. 拨号

    暂不赘述。

还有很多用法不一一列举。

向下传递数据

Intent中提供了一系列putExtra()方法的重载,可以把我们想要传递的数据暂存在Intent中,在启动另一个Activity后,只需要把这些数据从Intent中取出就可以了。

1
2
3
4
5
6
button1.setOnClickListener { 
val data = "Hello SecondActivity"
val intent = Intent(this, SecondActivity::class.java)
intent.putExtra("extra_data", data)
startActivity(intent)
}

调用的父类的getIntent() 方法,该方法会获取用于启动SecondActivity的Intent,然后调用getStringExtra()方法并传入相应的键值,就可以得到传递的数据了

1
2
3
4
5
6
7
8
class SecondActivity : AppCompatActivity() { 
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.second_layout)
val extraData = intent.getStringExtra("extra_data")
Log.d("SecondActivity", "extra data is $extraData")
}
}

向上返回数据

Activity类中还有一个用于启动Activity的startActivityForResult() 方法,但它期望在Activity销毁的时候能够返回一个结果给上一个Activity。

startActivityForResult()方法接收两个参数:第一个参数还是Intent;第二个参数是请求码,用于在之后的回调中判断数据的来源。

1
2
3
4
button1.setOnClickListener { 
val intent = Intent(this, SecondActivity::class.java)
startActivityForResult(intent, 1)
}

setResult() 方法接收两个参数:第一个参数用于向上一个Activity返回处理结果,一般只使用RESULT_OK或RESULT_CANCELED这两个值;第二个参数则把带有数据的Intent传递回去。最后调用了finish()方法来销毁当前Activity。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class SecondActivity : AppCompatActivity() { 

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.second_layout)
button2.setOnClickListener {
val intent = Intent()
intent.putExtra("data_return", "Hello FirstActivity")
setResult(RESULT_OK, intent)
finish()
}
}

}

由于我们是使用startActivityForResult()方法来启动SecondActivity的,在SecondActivity被销毁之后会回调上一个Activity的onActivityResult() 方法,因此我们需要重写这个方法来得到返回的数据。

onActivityResult()方法带有3个参数:第一个参数requestCode,即我们在启动Activity时传入的请求码;第二个参数resultCode,即我们在返回数据时传入的处理结果;第三个参数data,即携带着返回数据的Intent。

1
2
3
4
5
6
7
8
9
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
1 -> if (resultCode == RESULT_OK) {
val returnedData = data?.getStringExtra("data_return")
Log.d("FirstActivity", "returned data is $returnedData")
}
}
}

如果用户在SecondActivity中并不是通过点击按钮,而是通过按下Back键回到FirstActivity,可以通过在SecondActivity中重写onBackPressed() 方法来解决这个问题。

生命周期

Android是使用任务(task)来管理Activity的,而每当我们按下Back键或调用finish()方法去销毁一个Activity时,处于栈顶的Activity就会出栈。系统总是会显示处于栈顶的Activity给用户。

本小节了解完整生存期、可见生存期、前台生存期概念即可。

QQ_1736482183719

  • onCreate() 它会在Activity第一次被创建的时候调用。你应该在这个方法中完成Activity的初始化操作,比如加载布局、绑定事件等;
  • onStart() 这个方法在Activity由不可见变为可见的时候调用;
  • onResume() 这个方法在Activity准备好和用户进行交互的时候调用;
  • onPause() 这个方法在系统准备去启动或者恢复另一个Activity的时候调用;
  • onStop() 这个方法在Activity完全不可见的时候调用;
  • onDestroy() 这个方法在Activity被销毁之前调用,之后Activity的状态将变为销毁状态;
  • onRestart() 这个方法在Activity由停止状态变为运行状态之前调用,也就是Activity被重新启动了。

小知识:android:theme属性,用于给当前Activity指定主题,@style/Theme.AppCompat.Dialog则毫无疑问是使用对话框式的主题。

回收时数据保存

Activity中提供了一个onSaveInstanceState() 回调方法,这个方法可以保证在Activity被回收之前一定会被调用。

onSaveInstanceState()方法会携带一个Bundle类型的参数,Bundle提供了一系列的方法用于保存数据,比如可以使用putString()方法保存字符串,使用putInt()方法保存整型数据,以此类推。每个保存方法需要传入两个参数,第一个参数是键,用于后面从Bundle中取值,第二个参数是真正要保存的内容。

最后,保存的Bundle类型的参数将传入onCreate()方法。

1
2
3
4
5
6
7
8
9
10
override fun onCreate(savedInstanceState: Bundle?) { 
super.onCreate(savedInstanceState)
Log.d(tag, "onCreate")
setContentView(R.layout.activity_main)
if (savedInstanceState != null) {
val tempData = savedInstanceState.getString("data_key")
Log.d(tag, "tempData is $tempData")
}
...
}

当手机的屏幕发生旋转的时候,Activity也会经历一个重新创建的过程,因而在这种情况下,Activity中的数据也会丢失。虽然这个问题同样可以通过onSaveInstanceState()方法来解决,但是一般不太建议这么做,因为对于横竖屏旋转的情况,现在有更加优雅的解决方案。

启动模式

启动模式一共有4种,分别是standard、singleTop、singleTask和singleInstance,可以在AndroidManifest.xml中通过给<activity>标签指定android:launchMode属性来选择启动模式。

  1. standard(默认):不考虑Activity重复,每次创建新的Activity;

  2. singleTop:只考虑栈顶Activity不重复;

  3. singleTask:考虑Activity的重复问题,但是回退到Activity时前面的所有Activity将出栈丢失;

  4. singleInstance:使用单独的返回栈来管理这个Activity,不管是哪个应用程序来访问这个Activity,都共用同一个返回栈,解决共享Activity实例的问题。

    QQ_1736515722571

    SecondActivity不同于FirstActivity和ThirdActivity,SecondActivity存放在一个单独的返回栈里,而且这个栈中只有SecondActivity这一个Activity。按下Back键进行返回,ThirdActivity返回到了FirstActivity,再按下Back键又会返回到SecondActivity,再按下Back键才会退出程序。

实践

知晓当前Activity

创建BaseActivity,让其他Activity继承,同时在BaseActivity内部打印当前活动的Activity即可。

1
2
3
4
5
6
open class BaseActivity : AppCompatActivity() { 
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d("BaseActivity", javaClass.simpleName)
}
}

Kotlin中的javaClass表示获取当前实例的Class对象,相当于在Java中调用getClass()方法;而Kotlin中的BaseActivity::class.java表示获取BaseActivity类的Class对象,相当于在Java中调用BaseActivity.class。注意,后者显式Intent切换Activity会用到。

思路点拨:子类重写onCreate方法时会覆盖父类方法,但是为什么这样可行呢?因为子类的onCreate方法第一句按照规定应为调用父类的onCreate方法,因此生效。

退出Activity

使用一次Back按键达到退出目的,思路是使用一个专门的类对所有的Activity进行管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
object ActivityCollector { 
private val activities = ArrayList<Activity>()
fun addActivity(activity: Activity) {
activities.add(activity)
}
fun removeActivity(activity: Activity) {
activities.remove(activity)
}
fun finishAll() {
for (activity in activities) {
if (!activity.isFinishing) {
activity.finish()
}
}
activities.clear()
}
}
open class BaseActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d("BaseActivity", javaClass.simpleName)
ActivityCollector.addActivity(this)
}

override fun onDestroy() {
super.onDestroy()
ActivityCollector.removeActivity(this)
}
}

class ThirdActivity : BaseActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d("ThirdActivity", "Task id is $taskId")
setContentView(R.layout.third_layout)
button3.setOnClickListener {
ActivityCollector.finishAll()
}
}
}

当然你还可以在销毁所有Activity的代码后面再加上杀掉当前进程的代码,以保证程序完全退出:

1
android.os.Process.killProcess(android.os.Process.myPid()) 

killProcess()方法用于杀掉一个进程,它接收一个进程id参数,我们可以通过myPid()方法来获得当前程序的进程id。需要注意的是,killProcess()方法只能用于杀掉当前程序的进程,不能用于杀掉其他程序。

启动规范

简化启动流程,并且接口规范化,利于团队化编程。养成一个良好的习惯,给你编写的每个Activity都添加类似的启动方法,这样不仅可以让启动Activity变得非常简单,还可以节省不少你同事过来询问你的时间。

1
2
3
4
5
6
7
8
9
10
11
class SecondActivity : BaseActivity() { 
...
companion object {
fun actionStart(context: Context, data1: String, data2: String) {
val intent = Intent(context, SecondActivity::class.java)
intent.putExtra("param1", data1)
intent.putExtra("param2", data2)
context.startActivity(intent)
}
}
}

标准函数和静态方法

标准函数with、run和apply

前情提要:let标准函数,主要作用是配合?.操作符来进行辅助判空处理。

with

用于在连续调用同一个对象的多个方法时,让代码变得更加精简。

with函数接收两个参数:第一个参数可以是一个任意类型的对象,第二个参数是一个Lambda表达式。with函数会在Lambda表达式中提供第一个参数对象的上下文,并使用Lambda表达式中的最后一行代码作为返回值返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
val result = with(obj) { 
// 这里是obj的上下文
"value" // with函数的返回值
}

// 吃完所有水果,并将结果打印出来
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
val result = with(StringBuilder()) {
append("Start eating fruits.\n")
for (fruit in list) {
append(fruit).append("\n")
}
append("Ate all fruits.")
toString()
}
println(result)

run

run函数的用法和使用场景其实和with函数是非常类似的,只是稍微做了一些语法改动而已。

首先run函数通常不会直接调用,而是要在某个对象的基础上调用;其次run函数只接收一个Lambda参数,并且会在Lambda表达式中提供调用对象的上下文,使用Lambda表达式中的最后一行代码作为返回值返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
val result = obj.run { 
// 这里是obj的上下文
"value" // run函数的返回值
}

val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
val result = StringBuilder().run {
append("Start eating fruits.\n")
for (fruit in list) {
append(fruit).append("\n")
}
append("Ate all fruits.")
toString()
}
println(result)

apply

apply函数和run函数也是极其类似的,都要在某个对象上调用,并且只接收一个Lambda参数,也会在Lambda表达式中提供调用对象的上下文,但是apply函数无法指定返回值,而是会自动返回调用对象本身。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
val result = obj.apply { 
// 这里是obj的上下文
}
// result == obj


val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
val result = StringBuilder().apply {
append("Start eating fruits.\n")
for (fruit in list) {
append(fruit).append("\n")
}
append("Ate all fruits.")
}
println(result.toString())

由于apply函数无法指定返回值,只能返回调用对象本身,因此这里的result实际上是一个StringBuilder对象,所以我们在最后打印的时候还要再调用它的toString()方法才行。

定义静态方法

类静态方法

Kotlin极度弱化了静态方法这个概念,因为Kotlin提供了比静态方法更好用的语法特性,那就是单例类。

不过,使用单例类的写法会将整个类中的所有方法全部变成类似于静态方法的调用方式,而如果我们只是希望让类中的某一个方法变成静态方法的调用方式,可以使用companion object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Util { 

fun doAction1() {
println("do action1")
}

companion object {

fun doAction2() {
println("do action2")
}

}

}

doAction2()方法其实也并不是静态方法,实质在Util类的内部创建一个伴生类,因此调用Util.doAction2()方法实际上就是调用了Util类中伴生对象的doAction2()方法。

静态方法

解决在Java代码中以静态方法的形式去调用companion object和单例类会报错,因为这些不是真正的静态方法。

要定义真正的静态方法,Kotlin仍然提供了两种实现方式:注解和顶层方法

而如果我们给单例类或companion object中的方法加上 @JvmStatic注解,那么Kotlin编译器就会将这些方法编译成真正的静态方法。

注意,@JvmStatic注解只能加在单例类或companion object中的方法上,如果你尝试加在一个普通方法上,会直接提示语法错误。

顶层方法指的是那些没有定义在任何类中的方法。Kotlin编译器会将所有的顶层方法全部编译成静态方法。

在Java代码中调用,因为Java中没有顶层方法这个概念,且所有的方法必须定义在类中。Kotlin编译器会自动创建一个叫作HelperKt的Java类,doSomething()方法就是以静态方法的形式定义在HelperKt类里面的,因此在Java中使用HelperKt.doSomething()的写法来调用就可以了。

Kotlin语法基础

本章节建议重点学习!!!基础不牢,地动山摇!!

QQ_1736060951550

语法

这里列举的都是Kotlin和其他语言相比所独有的特征,Kotlin基本语法需要习惯化的点:

  1. 只使用val(不可变)var(可变) 来定义变量,强制指定使用冒号加类型;
  2. 基本数据类型类似Python是对象,更加强调面向对象编程;
  3. Kotlin完全舍弃了new关键字,因此创建匿名类实例的时候就不能再使用new了,而是改用了object关键字。

函数

以下是函数的一些语法糖:

  1. 当一个函数中只有一行代码时,Kotlin允许我们不必编写函数体,可以直接将唯一的一行代码写在函数定义的尾部,中间用等号连接即可(注意Kotlin的自动推导很厉害的,一行代码如果有返回值,可以不用显示强调返回类型,会自动推导出来)

    1
    2
    3
    // 下面两句话等价,并且编辑器提示会正确出现返回值类型
    fun largerNumber(num1: Int, num2: Int): Int = max(num1, num2)
    fun largerNumber(num1: Int, num2: Int) = max(num1, num2)

条件

if的语法糖

  1. Kotlin中的if语句有一个额外的功能,它是可以有返回值的,返回值就是if语句每一个条件中最后一行代码的返回值;

    1
    2
    3
    4
    5
    fun largerNumber(num1: Int, num2: Int) = if (num1 > num2) { 
    num1
    } else {
    num2
    }

when语法和语法糖

  1. when语句允许传入一个任意类型的参数,然后可以在when的结构体中定义一系列的条件,格式 是:

    1
    匹配值 -> { 执行逻辑 } 
  2. 基本匹配和类型匹配:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 基本匹配
    fun getScore(name: String) = when (name) {
    "Tom" -> 86
    "Jim" -> 77
    "Jack" -> 95
    "Lily" -> 100
    else -> 0
    }
    // 类型匹配 is 是核心
    fun checkNumber(num: Number) {
    when (num) {
    is Int -> println("number is Int")
    is Double -> println("number is Double")
    else -> println("number not support")
    }
    }
  3. 不带参数写法,拓展性强:

    1
    2
    3
    4
    5
    6
    7
    fun getScore(name: String) = when { 
    name == "Tom" -> 86
    name == "Jim" -> 77
    name == "Jack" -> 95
    name == "Lily" -> 100
    else -> 0
    }

循环

区间

  1. 区间[0, 10]

    1
    val range = 0..10 
  2. 区间[0, 10)

    1
    val range = 0 until 10 
  3. 步进

    1
    val range = 0 .. 10 step 2
  4. 倒序

    1
    val range = 10 downTo 0

for-in

已丢弃掉for-i循环,转投for-in

对象

继承与构造

  1. 在Kotlin中任何一个非抽象类默认都是不可以被继承的,加上open关键字之后,类就允许被继承;

  2. 主构造函数的特点是没有函数体,Kotlin给我们提供了一个init结构体,所有主构造函数中的逻辑都可以写在里面:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    open class Person { 
    ...
    }
    class Student(val sno: String, val grade: Int) : Person() {
    init {
    println("sno is " + sno)
    println("grade is " + grade)
    }
    }
  3. 调用父类构造函数,传递参数不需要变量声明,否则会错误。

    1
    2
    3
    4
    5
    6
    7
    open class Person(val name: String, val age: Int) { 
    ...
    }
    class Student(val sno: String, val grade: Int, name: String, age: Int) :
    Person(name, age) {
    ...
    }

    我们在Student类的主构造函数中增加name和age这两个字段时,不能再将它们声明成val,因为在主构造函数中声明成val或者var的参数将自动成为该类的字段,这就会导致和父类中同名的name和age字段造成冲突。因此,这里的name和age参数前面我们不用加任何关键字,让它的作用域仅限定在主构造函数当中即可

  4. Kotlin规定,当一个类既有主构造函数又有次构造函数时,所有的次构造函数都必须调用主构造函数(包括间接调用)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    open class Person(val name: String, val age: Int) { 
    ...
    }
    class Student(val sno: String, val grade: Int, name: String, age: Int) :
    Person(name, age) {
    // 次构造函数1
    constructor(name: String, age: Int) : this("", 0, name, age) {
    }
    // 次构造函数2
    constructor() : this("", 0) {
    }
    }

    val student1 = Student() // 本质是先调用次构造函数2,由次构造函数2调用次构造函数1,次构造函数1再调用主构造函数
    val student2 = Student("Jack", 19) // 本质是先调用次构造函数1,次构造函数1再调用主构造函数
    val student3 = Student("a123", 5, "Jack", 19) // 直接调用主构造函数

  5. 只有次构造函数,没有主构造函数

    1
    2
    3
    4
    5
    6
    7
    open class Person(val name: String, val age: Int) { 
    ...
    }
    class Student : Person {
    constructor(name: String, age: Int) : super(name, age) {
    }
    }

    总之,必须调用到root类的主构造函数,这是本质。注意这里的代码变化,首先Student类的后面没有显式地定义主构造函数,同时又因为定义了次构造函数,所以现在Student类是没有主构造函数的。那么既然没有主构造函数,继承 Person类的时候也就不需要再加上括号了。

接口

  1. 接口函数使用interface定义;
  2. 实现接口和继承类似,但是接口没有构造函数,因此不用加括号;
  3. Kotlin 使用override关键字来重写父类或者实现接口中的函数;
  4. 函数接受接口类作为参数,在函数内部调用接口方法,叫面向接口编程,简称多态

可见性修饰符

  1. private,当前类内部可见;
  2. public,所有类可见,是默认项
  3. protected,当前类和子项可见;
  4. internal,同一模块中可见。

数据类和单例类

  1. 数据类通常需要重写equals(),hashCode(),toString() 这几个方法;
  2. Kotlin 只需要声明data关键字,表明这个类是一个数据类,其中固定和无实际逻辑的方法自动生成;
  3. Kotlin 中,单例类生成使用object关键字取代class关键字即可,不需要私有化构造函数;
  4. 调用单例类,不需要实例化,可以类似静态方法的调用方式执行,但是其实背后是Kotlin 自动创建了一个实例。

Lambda编程

Lambda表达式的语法结构:

1
{参数名1 : 参数类型, 参数名2 : 参数类型 -> 函数体}

ArrayList和HashSet

ArrayList简称List,HashSet简称Set。

  1. Kotlin 专门提供内置函数listOf() 来简化初始化集合,创建的是一个不可变的集合;
  2. mutableListOf() 函数创建的是一个可变的集合;
  3. Set集合不可以存放重复元素,这是异于List 集合的最大不同。
  4. Kotlin 同样提供了setOf()mutableSetOf() ,以简化set用法

HashMap

HashMap简称map。

  1. Kotlin 中不建议使用put()get() 方法对map进行添加和读取数据操作,而是更加推荐使用类似于数组下标的语法结构。

    1
    2
    map["Apple"] = 1 // 写入
    val number = map["Apple"] // 读取
  2. Kotlin 同样提供了mapOf()mutableMapOf() ,以简化map用法。

    1
    val map = mapOf("Apple" to 1, "Banana" to 2, "Orange" to 3, "Pear" to 4, "Grape" to 5) 
  3. 键值对使用to关联,而to不是关键字,这是一种infix函数,在后续学习。

集合函数式API

  1. maxBy就是一个普通的函数,它接收的是一个Lambda类型的参数,并且会在遍历集合时将每次遍历的值作为参数传递给Lambda表达式。maxBy函数的工作原理是根据 我们传入的条件来遍历集合,从而找到该条件下的最大值;

    1. Kotlin 规定,当Lambda参数是函数最后一个参数时,可以将Lambda表达式移到函数括号外面;

      1
      val maxLengthFruit = list.maxBy() { fruit: String -> fruit.length } 
    2. 如果Lambda参数是函数唯一一个参数的话,还可以将函数括号省略;

      1
      val maxLengthFruit = list.maxBy { fruit -> fruit.length } 
    3. 当Lambda表达式的参数列表中只有一个参数时,不必声明参数名,可以使用it 关键字替代。

      1
      val maxLengthFruit = list.maxBy { it.length } 
  2. map 它用于将集合中的每个元素都映射成一个另外的值,映射的规则在Lambda表达式中指定,最终生成一个新的集合;

    1
    val newList = list.map { it.toUpperCase() }  // 将名字全部转化为大写
  3. filter是用来过滤集合中的数据的;

    1
    val newList = list.filter { it.length <= 5 } // 获取长度小于5的名字
  4. any用于判断集合中是否至少存在一个元素满足指定条件;

  5. all用于判断集合中是否所有元素都满足指定条件。

Java函数式API

针对Runnable接口进行讲解。

  1. 因为Runnable类中只有一个待实现方法,即使这里没有显式地重写run()方法,Kotlin也能自动明白Runnable后面的Lambda表达式就是要在run()方法中实现的内容,否则使用override重写多个方法函数。

    1
    2
    3
    Thread(Runnable { 
    println("Thread is running")
    }).start()
  2. 一个Java方法的参数列表中有且仅有一个Java单抽象方法接口参数,我们还可以将接口名进行省略;

    1
    2
    3
    Thread({ 
    println("Thread is running")
    }).start()
  3. 当Lambda表达式是方法的最后一个参数时,可以将Lambda表达式移到方法括号的外面。同时,如果Lambda表达式还是方法的唯一一个参数,还可以将方法的括号省略。

    1
    2
    3
    Thread { 
    println("Thread is running")
    }.start()

空指针检查

Kotlin默认所有的参数和变量都不可为空。

判空辅助工具

  1. 最常用的是 ?. 操作符,这个操作符的作用非常好理解,就是当对象不为空时正常调用相应的方法,当对象为空时则什么都不做;

    1
    a?.doSomething() 
  2. 另外一个非常常用的 ?: 操作符,这个操作符的左右两边都接收一个表达式,如果左边表达式的结果不为空就返回左边表达式的结果,否则就返回右边表达式的结果;

    1
    val c = a ?: b 
  3. 如果我们想要强行通过编译,可以使用非空断言工具,写法是在对象的后面加上 !! ,如下所示:

    1
    2
    3
    4
    fun printUpperCase() { 
    val upperCase = content!!.toUpperCase()
    println(upperCase)
    }
  4. 一个比较与众不同的辅助工具——let。let既不是操作符,也不是什么关键字,而是一个函数。这个函数提供了函数式API的编程接口,并将原始调用对象作为参数传递到 Lambda表达式中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    obj.let { obj2 -> 
    // 编写具体的业务逻辑
    }

    var study: Study? = null
    fun doStudy(study: Study?) {
    study?.let {
    it.readBooks()
    it.doHomework()
    }
    }

    这里调用了obj对象的let函数,然后Lambda表达式中的代码就会立即执行,并且这个obj对象本身还会作为参数传递到Lambda表达式中。不过,为了防止变量重名,这里我将参数名改成了obj2,但实际上它们是同一个对象,这就是let函数的作用。

  5. 全局变量的值随时都有可能被其他线程所修改,即使做了判空处理,仍然无法保证if语句中的study变量没有空指针风险,仍会报错:

    1
    2
    3
    4
    5
    6
    7
    var study: Study? = null
    fun doStudy() {
    if (study != null) {
    study.readBooks()
    study.doHomework()
    }
    }

Kotlin 中的小魔术

  1. 字符串内嵌表达式,Kotlin允许我们在字符串里嵌入 ${} 这种语法结构的表达式,并在运行时使用表达式执行的结果替代这一部分内容:

    1
    "hello, ${obj.name}. nice to meet you!" 
  2. 函数的参数默认值,可以通过键值对的方式来传参。

    次构造函数在Kotlin中很少用,因为Kotlin 提供了给函数设定参数默认值的功能,它在很大程度上能够替代次构造函数的作用。

第一行Android代码

本章节主要介绍了安卓大致背景,以及安装Android Studio,当前版本为Meerkat | 2024.3.1 Canary 7,并大致介绍安卓项目创建、目录等,这一章主要是背景知识居多,粗略看一遍有印象即可。

QQ_1736060400046

创建项目

创建项目需要注意换源以及配置模拟器

注意选用Empty View Activity!!!否则没有AppCompatActivity!

换源

  1. 打开gradle/wrapper/gradle-wrapper.properties文件,替换如下:

    1
    2
    # 其中gradle-8.11.1-bin.zip修改为你自己的
    distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.11.1-bin.zip
  2. 打开settings.gradle.kts文件,替换如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    pluginManagement {
    repositories {
    // 改为阿里云的镜像地址
    maven { setUrl("https://maven.aliyun.com/repository/central") }
    maven { setUrl("https://maven.aliyun.com/repository/jcenter") }
    maven { setUrl("https://maven.aliyun.com/repository/google") }
    maven { setUrl("https://maven.aliyun.com/repository/gradle-plugin") }
    maven { setUrl("https://maven.aliyun.com/repository/public") }
    maven { setUrl("https://jitpack.io") }
    maven { setUrl("https://maven.aliyun.com/nexus/content/groups/public/") }
    maven { setUrl("https://maven.aliyun.com/nexus/content/repositories/jcenter") }
    gradlePluginPortal()
    google()
    mavenCentral()
    }
    }
    dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
    // 改为阿里云的镜像地址
    maven { setUrl("https://maven.aliyun.com/repository/central") }
    maven { setUrl("https://maven.aliyun.com/repository/jcenter") }
    maven { setUrl("https://maven.aliyun.com/repository/google") }
    maven { setUrl("https://maven.aliyun.com/repository/gradle-plugin") }
    maven { setUrl("https://maven.aliyun.com/repository/public") }
    maven { setUrl("https://jitpack.io") }
    google()
    mavenCentral()
    }
    }

    ......

至此,重新构建即可。

模拟器

对于模拟器,选择Pixel,使用API 29

注意模拟器如果卡死,重启也无用,直接删除重新创建即可。

日志

QQ_1736060520838

当前日志在小猫图标处进行查看。

筛选器

常用使用方式如下:

  • tag:与日志条目的 tag 字段匹配。
  • package:与日志记录应用的软件包名称匹配。
  • process:与日志记录应用的进程名称匹配。
  • message:与日志条目的消息部分匹配。
  • level:与指定或更高严重级别的日志匹配,例如 debug
  • age:如果条目时间戳是最近的,则匹配。值要指定为数字,后跟表示时间单位的字母:s 表示秒,m 表示分钟,h 表示小时,d 表示天。例如,age: 5m 只会过滤过去 5 分钟内记录的消息。