安卓开发(指原生开发)和普通的Java开发,web开发最大的不同是什么?界面。老早的Java awt/swing以及现在的JavaFX,里面的UI库和安卓完全不是一个体系。至于web开发的各种UI框架,和安卓UI库也是天壤之别。所以说,掌握了安卓的界面开发,也就是掌握了安卓开发的一半。
我2015年前后做过一段时间的安卓开发,七八年过后,安卓的版本升级了,API也发生了不小的变化。最让人头疼的是,一个需求、一个界面功能可能会有多种实现方式,有开源的,还有标准库的,让我们实现时不得不先考察一番彼此优缺。
为此,不得不记录一下个人开发过程中遇到的主要疑难点。
Android界面库从Android View为基础的UI库目前已经进化到Jetpack Compose界面库,差异比较大。本文会简单介绍两者开发的基础知识。 View 系统可以和Compose系统共存,你可以逐步改写和迁移。
Android开发基于Java语言,除了Java SDK提供的标准API外,肯定得有一个支持移动特性的库,早期这个库名称叫android support库。最早的 Support 库发布于 2011 年,版本号为:android.support.v4 ,2013 年在 v4 的基础上,Android 团队发布了 v7 库,版本号为:android.support.v7,之后还发布了用于特定场景的 v8、v13、v14、v17。
由于support库版本增长混乱,还有一些重复依赖,Google 重写了 Support 库,推出了新的 AndroidX,AndroidX 将原有的 Support 库拆分为 85 个大大小小的支持库。
新版支持库AndroidX在Android 9.0(API 级别28)中发布,也称之为Jetpack库。Jetpack 是一堆实用的组件。对开发者而言,Jetpack 和 AndroidX 可以认为是同一个东西,从产品的维度它叫做 Jetpack,从技术的维度它叫做 AndroidX。Jetpack组件库包含四个基本部分:
Jetpack带来了一个新的编程框架:Jetpack Compose,它是用于构建原生 Android 界面的新工具包。可简化并加快 Android 上的界面开发,使用更少的代码、强大的工具和直观的 Kotlin API,快速打造生动而精彩的应用。重点:Compose 是一个声明性界面框架。声明性界面工具包是相当于面向对象的命令式界面工具包而言的。Compose 提供声明性 API,让您可在不以命令方式改变前端视图的情况下呈现应用界面。这个和web端常用的视图数据绑定有点类似,详细的介绍和示例可以看看Jetpack Compose
Android项目架构经历了从MVC、MVP到MVVM的演化,从2015年开始,Google官方开始对Android项目架构做出指导,随后推出DataBinding组件以及后边一系列的Jetpack组件来帮助开发者优化项目架构。Google官方给出的这一套指导架构就是MVVM。MVVM是 Model-View-ViewModel 的简称。这一架构在一定程度上解决了MVP架构中存在的问题。虽然近期官方的指导架构由MVVM变为了MVI,但MVVM依然是目前Android项目的主流架构。
MVVM架构的本质是数据驱动,它的最大的特点是单向依赖。MVVM架构通过观察者模式让ViewModel与View解耦,实现了View依赖ViewModel,ViewModel依赖Model的单向依赖。
先看一个关系图:
View 和 ViewGroup 的区别:
再看一下界面的绘制流程:
能把组件按照需求摆放整齐,该对齐的对齐,该滚动的滚动,那你的界面就搞掂了一半。界面开发=UI布局+事件处理。
AbsoluteLayout:绝对布局,每个组件通过指定layour_x和layour_y按坐标定位,不灵活,不推荐使用。
android.widget.LinerLayout
线性布局用得非常多也是相对比较简单的布局,组件按照水平或垂直方向依次排下来,通过android:orientation控制方向。
android.widget.FrameLayout
通常用来显示单一项widget,通过layout_gravity来调节组件相对于容器的位置。注意android:gravity和android:layout_gravity的差别,前者是调节组件自身。
TableLayout和GridLayout
行列布局器。LinerLayout只有一行或一列,而TableLayout和GridLayout可以有多行多列。TableLayout继承自LinearLayout,本质上仍然是线性布局管理器。表格布局采用行、列的形式来管理UI组件,并不需要明确地声明包含多少行、多少列,而是通过添加TableRow、其他组件来控制表格的行数和列数。GridLayout比TableLayout更强大,是Android4.0新增的布局。
androidx.coordinatorlayout.widget.CoordinatorLayout
在android5.0的时候引入了CoordinatorLayout、AppBarLayout、Toolbar、CollapsingToolbarLayout等等一系列的新控件,CoordinatorLayout遵循Material 风格。协调什么?协调里面组件的联动,通过设置app:layout_behavior属性,只有CoordinatorLayout的直接子布局才能响应。CoordinatorLayout的使用核心是Behavior,简单地说,如果一个子view的行为会影响到依赖它的其它子view,就可以采用CoordinatorLayout。所以这个布局一般用于一些特效中。例如CoordinatorLayout+AppBarLayout+CollapsingToolbarLayout的结合使用。
android.widget.RelativeLayout和androidx.constraintlayout.widget.ConstraintLayout
这两个布局比较相似,都可以创建扁平视图层次结构(无嵌套视图组),适用于复杂的大型布局(组件多)。其中所有的视图均根据同级视图与父布局之间的关系进行布局。相对布局,相对什么?相对于容器,相对于其他组件,例如layout_alignParentLeft指定和容器左对齐,layout_toRightOf表示位于指定组件的右侧。ConstraintLayout的灵活性要高于 RelativeLayout,并且更易于与 Android Studio 的布局编辑器配合使用。有编辑器的支持,通过拖放的形式(而非修改 XML)来构建布局,能达到传统桌面胖客户端UI开发中所见即所得的效果。
androidx.swiperefreshlayout.widget.SwipeRefreshLayout
com.google.android.material.appbar.AppBarLayout
继承于LinearLayout的,默认的方向是Vertical。必须在它的子view上设置app:layout_scrollFlags属性或者是在代码中调用setScrollFlags()设置这个属性。其作用就是Content布局文件中某个可垂直滚动的View发生滚动事件时,其内部的子View也跟随发生相应变化。
ConstraintLayout是默认的布局,比较灵活,能减少布局的嵌套,性能也不错。在 View 系统中,建议使用 ConstraintLayout 来创建复杂的大型布局,因为扁平视图层次结构比嵌套视图的效果更好。
ConstraintLayout 包含引导线、屏障线和链三个基本概念。引导线分为垂直和水平引导线,屏障线会引用多个可组合项,从而根据所指定边中处于最边缘位置的 widget 创建虚拟引导线,链在单条轴(水平或垂直方向)上提供类似于组的行为。另一条轴可单独约束。
下面给出一个基本例子。
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="10dp">
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="100dp"
android:layout_marginTop="100dp"
android:text="button1"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="button2"
app:layout_constraintHorizontal_bias="0.8"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/button1" />
<TextView
android:id="@+id/tv1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="20"
android:textColor="@color/black"
android:textSize="50sp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="¥"
android:textColor="@color/black"
android:textSize="20sp"
app:layout_constraintBaseline_toBaselineOf="@id/tv1"
app:layout_constraintStart_toEndOf="@id/tv1" />
androidx.constraintlayout.widget.ConstraintLayout>
使用ConstraintLayout的一大好处是可以通过Android Studio可视化设计界面布局:
通过设置上下左右的Constraint属性就能方便地控制组件的位置。上图对应的xml内容:
<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">
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="20dp"
android:text="Button1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textView2"
android:layout_width="235dp"
android:layout_height="94dp"
android:layout_marginStart="100dp"
android:layout_marginTop="148dp"
android:text="TextView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/button" />
<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="44dp"
android:layout_marginTop="20dp"
android:text="Button2"
app:layout_constraintStart_toEndOf="@+id/button"
app:layout_constraintTop_toTopOf="parent" />
androidx.constraintlayout.widget.ConstraintLayout>
实现onLayout方法即可。
<fragment android:id="@id/main_fragment"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_above="@id/tab_bar_container"
android:layout_alignParentTop="true"
class="io.toutiao.android.ui.fragment.MainFragment" />
public void onCreate() {
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
Fragment fg = fm.findFragmentById(R.id.frag_container); // 实例化
ft.add(R.id.frame_container, fg); //将fragment添加到布局当中
ft.commit();
// 显示和隐藏
ft.show(fg);
ft.hide(fg);
}
Context数量 = Activity数量 + Service数量 + 1
也就是说,一个应用程序中可以有多个Activity和多个Service,但是只能有一个Application。
我们经常把一些全局的对象如数据库操作工具类,放在Application里,那如何获得这个全局的Application对象?
Context context = getApplicationContext();
Application application = context.getApplication();
可以在任意的 Activity 中直接调用 getApplication() 方法:
Application application = getApplication();
对于Kotlin,可以这么写:
class MyApplication : Application() {
companion object {
lateinit var instance: Application
}
init {
instance = this
}
}
然后:
val appContext: Context = MyApplication.application.applicationContext
com.google.android.material.tabs.TabLayout
强大的ViewPager2
androidx.appcompat.widget.Toolbar
TODO
TODO
上图是最常用的三种布局:列式,行式和盒式。
例如:
@Composable
fun ConstraintLayoutContent() {
ConstraintLayout {
// Create references for the composables to constrain
val (button, text) = createRefs()
Button(
onClick = { /* Do something */ },
// Assign reference "button" to the Button composable
// and constrain it to the top of the ConstraintLayout
modifier = Modifier.constrainAs(button) {
top.linkTo(parent.top, margin = 16.dp)
}
) {
Text("Button")
}
// Assign reference "text" to the Text composable
// and constrain it to the bottom of the Button composable
Text("Text", Modifier.constrainAs(text) {
top.linkTo(button.bottom, margin = 16.dp)
})
}
}
上面组合式函数等价于我们在view系统中的定义。