MAD,现代安卓开发技术:Android 领域开发方式的重大变革~(2)

简介: MAD,现代安卓开发技术:Android 领域开发方式的重大变革~(2)

5.Jetpack

20200308234636925.jpg

Jetpack 单词的本意是火箭人,框架的 Logo 也可以看出来是个绑着火箭的 Android。Google 用它命名,含义非常明显,希望这些框架能够成为 Android 开发的助推器:助力 App 开发,体验飞速提升。


Jetpack 分为架构、UI、基础功能和特定功能等几个方面,其中架构板块是全新设计的,涵盖了 Google 花费大量精力开发的系列框架,是本章节着力讲解的方面。


架构以外的部分实际上是 AOSP 本身的一些组件进行优化之后集成到了Jetpack 体系内而已,这里不再提及。


架构:全新设计,框架的核心

以外:AOSP 本身组件的重新设计

UI

基础功能

特定功能

1832b220aa754cd18c504acc7686a560.png

Jetpack 具备如下的优势供我们在实现某块功能的时候收腰选择:


提供 Android 平台的最佳实践

消除样板代码

不同版本、厂商上达到设备一致性的框架表现

Google 官方稳定的指导、维护和持续升级

如果对 Jetpack 的背景由来感兴趣的朋友可以看我之前写的一篇文章:「从Preference组件的更迭看Jetpack的前世今生」。下面,我们选取 Jetpack 中几个典型的框架来了解和学习下它具体的优势。

5.1 View Binding

通常的话绑定布局里的 View 实例有哪些办法?又有哪些缺点?

通常做法 缺点
findViewById() NPE 风险、大量的绑定代码、类型转换危险
@ButterKnife NPE 风险、额外的注解代码、不适用于多模块项目(APT 工具解析 Library 受限)
KAE 插件 NPE 风险、操作其他布局的风险、Kotlin 语言独占、已经废弃

AS 现在默认采用 ViewBinding 框架帮我们绑定 View。

来简单了解一下它的用法:

<!--result_profile.xml-->
<LinearLayout ... >
    <TextView android:id="@+id/name" />
</LinearLayout>

ViewBinding 框架初始化之后,无需额外的绑定处理,即可直接操作 View 实例。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle) {
        super.onCreate(savedInstanceState)
        val binding = ResultProfileBinding.inflate(layoutInflater)
        setContentView(binding.root)
        binding.name.text = "Hello world"
    }
}

原理比较简单:编译器将生成布局同名的绑定类文件,然后在初始化的时候将布局里的 Root View 和其他预设了 ID 的 View 实例缓存起来。事实上无论是上面的注解,插件还是这个框架,其本质上都是通过 findViewById 实现的 View 绑定,只是进行了封装。


ViewBinding 框架能改善通常做法的缺陷,但也并非完美。特殊情况下仍需使用通常做法,比如操作布局以外的系统 View 实例 ContentView,ActionBar 等。

1672187238820.png

5.2 Data Binding

一般来说,将数据反映到 UI 上需要经过如下步骤:


创建 UI 布局

绑定布局中 View 实例

数据逐一更新到 View 的对应属性

而 DataBinding 框架可以免去上面的步骤 2 和 3。它需要我们在步骤 1 的布局当中就声明好数据和 UI 的关系,比如文本内容的数据来源、是否可见的逻辑条件等。

<layout ...>
    <data>
        <import type="android.view.View"/>
        <variable
            name="viewModel" type="com.example.splash.ViewModel" />
    </data>
    <LinearLayout ...>
        <TextView
            ...
            android:text="@{viewModel.userName}"
            android:visibility="@{viewModel.age >= 18 ? View.VISIBLE : View.GONE}"/>
    </LinearLayout>
</layout>

上述 DataBinding 布局展示的是当 ViewModel 的 age 属性大于 18 岁才显示文本,而文本内容来自于 ViewModel 的 userName 属性。

val binding = ResultProfileBinding.inflate(layoutInflater)
binding.viewModel = viewModel

Activity 中无需绑定和手动更新 View,像 ViewBinding 一样初始化之后指定数据来源即可,后续的 UI 展示和刷新将被自动触发。DataBinding 还有诸多妙用,大家可自行了解。

5.3 Lifecycle

监听 Activity 的生命周期并作出相应处理是 App 开发的重中之重,通常有如下两种思路。

通常思路 具体 缺点
基础 直接覆写 Activity 对应的生命周期函数 繁琐、高耦合
进阶 利用 Application#registerLifecycleCallback 统一管理 回调固定、需要区分各 Activity、逻辑侵入到 Application

而 Lifecycle 框架则可以高效管理生命周期。


使用 Lifecycle 框架需要先定义一个生命周期的观察者 LifecycleObserver,给生命周期相关处理添加上 OnLifecycleEvent 注解,并指定对应的生命状态。比如 onCreate 的时候执行初始化,onStart 的时候开始连接,onPause 的时候断开连接。

class MyLifecycleObserver(
    private val lifecycle: Lifecycle
) : LifecycleObserver {
    ...
    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
    fun init() {
        enabled = checkStatus()
    }
    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    fun start() {
        if (enabled) {
            connect()
        }
    }
    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    fun stop() {
        if (connected) {
            disconnect()
        }
    }
}

然后在对应的 Activity 里添加观察:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle) {
        ...
        MyLifecycleObserver(lifecycle).also { lifecycle.addObserver(it) }
    }
}

Lifecycle 的简单例子可以看出生命周期的管理变得很清晰,同时能和 Activity 的代码解耦。


继续看上面的小例子:假使初始化操作 init() 是异步耗时操作怎么办?


init 异步的话,onStart 状态回调的时候 init 可能没有执行完毕,这时候 start 的连接处理 connect 可能被跳过。这时候 Lifecycle 提供的 State 机制就可以派上用场了。


使用很简单,在异步初始化回调的时候再次执行一下开始链接的处理,但需要加上 STARTED 的 State 条件。这样既可以保证 onStart 时跳过连接之后能手动执行连接,还能保证只有在 Activity 处于 STARTED 及以后的状态下才执行连接。

class MyLifecycleObserver(...) : LifecycleObserver {
    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
    fun init() {
        checkStatus { result ->
            if (result) {
                enable()
            }
        }
    }
    fun enable() {
        enabled = true
        // 初始化完毕的时候确保只有在 STARTED 及以后的状态下执行连接
        if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
            if (!connected) {
                connect()
            }
        }
    }
    ...
}

5.4 Live Data

LiveData 是一种新型的可观察的数据存储框架,比如下面的使用示例,数据的封装和发射非常便捷:

class StockLiveData(symbol: String) : LiveData<BigDecimal>() {
    private val stockManager = StockManager(symbol)
    private val listener = { price: BigDecimal ->
        // 将请求到的数据发射出去
        value = price
    }
    // 画面活动状态下才请求
    override fun onActive() {
        stockManager.requestPriceUpdates(listener)
    }
    // 非活动状态下移除请求
    override fun onInactive() {
        stockManager.removeUpdates(listener)
    }
}
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        // 注册观察
        StockLiveData("Tesla").run { observe(this@MainActivity, Observer { ... })}
    }
}

支持异步传递数据以外,LiveData 还有很多优势:


与 Lifecycle 框架深度绑定

具有生命周期感知能力,数据不会发射给非活动状态的观察者

观察者销毁了自动释放数据,避免内存泄露

支持 Room 、Retrofit 框架

支持合并多个数据源统一观察的 MediatorLiveData(省去多个 LiveData 多次 observe 的丑陋处理))

但必须要说 LiveData 的定位和使用有这样那样的问题,官方的态度也一直在变,了解之后多使用 Flow 来完成异步的数据提供。

5.5 Room

Android 上开发数据库有哪些痛点?

  • 需要实现 SQLite 相关的 Helper 实例并实装初始化和 CRUD 等命令
  • 自行处理异步操作
  • Cursor实例需要小心处理
  • 字段对应关系
  • index 对齐
  • 关闭

官方推出的 Room 是在 SQLite 上提供了一个抽象层,通过注解简化数据库的开发。以便在充分利用 SQLite 的强大功能的同时,能够高效地访问数据库。

image.png

需要定义 Entity,Dao 以及 Database 三块即可完成数据库的配置,其他的数据库实现交由框架即可。

@Entity
class Movie() : BaseObservable() {
    @PrimaryKey(autoGenerate = true)
    var id = 0
    @ColumnInfo(name = "movie_name", defaultValue = "Harry Potter")
    lateinit var name: String
    ...
}
@Dao
interface MovieDao {
    @Insert
    fun insert(vararg movies: Movie?): LongArray?
    @Delete
    fun delete(movie: Movie?): Int
    @Update
    fun update(vararg movies: Movie?): Int
    @get:Query("SELECT * FROM movie")
    val allMovies: LiveData<List<Movie?>?>
}
@Database(entities = [Movie::class], version = 1)
abstract class MovieDataBase : RoomDatabase() {
    abstract fun movieDao(): MovieDao
    companion object {
        @Volatile
        private var sInstance: MovieDataBase? = null
        private const val DATA_BASE_NAME = "jetpack_movie.db"
        @JvmStatic
        fun getInstance(context: Context): MovieDataBase? {
            if (sInstance == null) {
                synchronized(MovieDataBase::class.java) {
                    if (sInstance == null) {
                        sInstance = createInstance(context)
                    }
                }
            }
            return sInstance
        }
        private fun createInstance(context: Context): MovieDataBase {
            return Room.databaseBuilder(context.applicationContext,
                    MovieDataBase::class.java, DATA_BASE_NAME).build()
        }
    }
}

在 ViewModel 初始化 DataBase 接口之后即可利用其提供的 DAO 接口执行操作,接着利用 LiveData 将数据发射到 UI。

class MovieViewModel(application: Application) : AndroidViewModel(application) {
    private val mediatorLiveData = MediatorLiveData<List<Movie?>?>()
    private val db: MovieDataBase?
    init {
        db = MovieDataBase.getInstance(application)
        if (db != null) {
            mediatorLiveData.addSource(db.movieDao().allMovies) { movieList ->
                if (db.databaseCreated.value != null) {
                    mediatorLiveData.postValue(movieList)
                }
            }
        };
    }
    fun getMovieList(owner: LifecycleOwner?, observer: Observer<List<Movie?>?>?) {
        if (owner != null && observer != null)
            mediatorLiveData.observe(owner, observer)
    }
}

Room 具备很多优势值得选作数据库的开发首选:


简洁高效,通过简单注解即可完成数据库的创建和 CRUD 封装

直接返回目标 POJO 实例,避免自行处理 Cursor 的风险

支持事务处理、数据库迁移、关系数据库等完整功能

支持 LiveData、Flow 等方式观察式查询

AS 的 Database Inspector 可以实时查看、编辑和部署 Room 的数据库

内置异步处理

5.6 View Model

ViewModel 框架和 AppCompat、Lifecycle 框架一样,可谓是 Jetpack 框架最重要的几个基础框架。虽功能不仅限于此,但我们想要借此探讨一下它在数据缓存方面的作用。


通常怎么处理横竖屏切换导致的 Activity 重绘?一可以选择自生自灭,只有部分 View 存在自行恢复的处理、也可以配置 ConfigurationChange 手动复原重要的状态、或者保存数据至 BundleState,在 onCreate 等时机去手动恢复。


得益于 ViewModel 实例在 Activity 重绘之后不销毁,其缓存的数据不受外部配置变化的影响,进而确保数据可以自动恢复数据,无需处理。

1832b220aa754cd18c504acc7686a560.png

这里定义一个 ViewModel,其中提供一个获取数据的方法,用来返回一个 30 岁名叫 Ellison 的朋友。Activity 取得 vm 实例之后观察数据的变化,并将数据反映到 UI 上。当屏幕方向变化后,名字和年龄的 TextView 可自动恢复,无需额外处理。

class PersonContextModel(application: Application) : AndroidViewModel(application) {
    val personLiveData = MutableLiveData<Person>()
    val personInWork: Unit
        get() {
            val testPerson = Person(30, "Ellison")
            personLiveData.postValue(testPerson)
        }
}
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        val model = ViewModelProvider(this).get(
            PersonContextModel::class.java
        )
        model.personLiveData.observe(this, Observer { person: Person ->
            binding.name.setText(person.name)
            binding.age.setText(person.age.toString())
        })
        binding.get.setOnClickListener({ view -> model.personInWork })
    }
}

ViewModel 的众多优势:


基于 Lifecycle 实现以注重生命周期的方式存储和管理界面相关的数据

画面销毁前存储 vm 实例并在重建后恢复,让数据可在发生屏幕旋转等配置更改后继续留存

可用于 Fragment 之间共享数据

作为数据和 UI 交互的媒介,用作 MVVM 架构的 VM 层

。。。

5.7 CameraX

完成一个相机预览的功能,使用 Camera2 的话需要如下诸多流程,会比较繁琐:

1832b220aa754cd18c504acc7686a560.png

而采用 CameraX 进行开发的话,几十行代码即可完成预览功能。

    private void setupCamera(PreviewView previewView) {
        ListenableFuture<ProcessCameraProvider> cameraProviderFuture =
                ProcessCameraProvider.getInstance(this);
        cameraProviderFuture.addListener(() -> {
            try {
                mCameraProvider = cameraProviderFuture.get();
                bindPreview(mCameraProvider, previewView);
            } catch (ExecutionException | InterruptedException e) {
                e.printStackTrace();
            }
        }, ContextCompat.getMainExecutor(this));
    }
    private void bindPreview(@NonNull ProcessCameraProvider cameraProvider,
                             PreviewView previewView) {
        mPreview = new Preview.Builder().build();
        mCamera = cameraProvider.bindToLifecycle(this,
                CameraSelector.DEFAULT_BACK_CAMERA, mPreview);
        mPreview.setSurfaceProvider(previewView.getSurfaceProvider());
    }

上面是 CameraX 的架构,可以看到其底层仍然是 Camera2,外加高度封装的接口,以及 Vendor 自定义的功能库。


使用它来作为全新的相机使用框架,具备很多优势:


代码简单,易用

自动绑定 Lifecycle,自动确定打开相机、何时创建拍摄会话以及何时停止和关闭

多设备的相机开发体验统一:国内外主流平台的设备都支持,国内的华米 OV 都在对这个框架支持和贡献

完美支持人像、HDR、夜间和美颜模式等拍摄模式的 Extensions

Monzo 利用 CameraX 缩减了 9,000 多行代码并使注册流程中的访问者流失率降低了 5 倍

这是一家银行服务公司并提供了同名应用,仅在移动设备上提供数字金融服务。他们的使命是向每个人传授生财之道。为了完成新客户注册,Monzo 应用会拍摄身份证明文件(例如护照、驾照或身份证)的图片,并拍摄自拍视频来证明身份证明文件属于申请者。


早期版本使用的是 camera2 API。在某些设备上会随机发生崩溃和异常行为,这导致 25% 的潜在客户无法继续进行身份证明拍摄和自拍视频步骤。

5.8 其他框架

篇幅有限,Jetpack 集合中还有非常多其他的优质框架等待大家的挖掘。

1672187478750.png在开发某个功能的时候,看看是否有轮子可用,尤其是官方的。

5.9 官方推荐的应用架构

我在官方的推荐架构上做了些补充,一般的 App 推荐采用如下的架构组件。

20200308234636925.jpg

尝试单 Activity 多 Fragment 的 UI 架构

通过 Navigation 导航

ViewModel 完成数据和 UI 交互

LiveData 观察数据

Room 和 DataStore 负责本地数据

Retrofit 负责网络数据

整体通过 Hilt 注入依赖

架构绝非固定模式,依实际需求和最佳实践自由搭配~



相关文章
|
12天前
|
开发框架 前端开发 Android开发
安卓与iOS开发中的跨平台策略
在移动应用开发的战场上,安卓和iOS两大阵营各据一方。随着技术的演进,跨平台开发框架成为开发者的新宠,旨在实现一次编码、多平台部署的梦想。本文将探讨跨平台开发的优势与挑战,并分享实用的开发技巧,帮助开发者在安卓和iOS的世界中游刃有余。
|
17天前
|
搜索推荐 Android开发 开发者
探索安卓开发中的自定义视图:打造个性化UI组件
【10月更文挑战第39天】在安卓开发的世界中,自定义视图是实现独特界面设计的关键。本文将引导你理解自定义视图的概念、创建流程,以及如何通过它们增强应用的用户体验。我们将从基础出发,逐步深入,最终让你能够自信地设计和实现专属的UI组件。
|
3天前
|
Java 调度 Android开发
安卓与iOS开发中的线程管理差异解析
在移动应用开发的广阔天地中,安卓和iOS两大平台各自拥有独特的魅力。如同东西方文化的差异,它们在处理多线程任务时也展现出不同的哲学。本文将带你穿梭于这两个平台之间,比较它们在线程管理上的核心理念、实现方式及性能考量,助你成为跨平台的编程高手。
|
19天前
|
Android开发 Swift iOS开发
探索安卓与iOS开发的差异和挑战
【10月更文挑战第37天】在移动应用开发的广阔舞台上,安卓和iOS这两大操作系统扮演着主角。它们各自拥有独特的特性、优势以及面临的开发挑战。本文将深入探讨这两个平台在开发过程中的主要差异,从编程语言到用户界面设计,再到市场分布的不同影响,旨在为开发者提供一个全面的视角,帮助他们更好地理解并应对在不同平台上进行应用开发时可能遇到的难题和机遇。
|
19天前
|
存储 API 开发工具
探索安卓开发:从基础到进阶
【10月更文挑战第37天】在这篇文章中,我们将一起探索安卓开发的奥秘。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的信息和建议。我们将从安卓开发的基础开始,逐步深入到更复杂的主题,如自定义组件、性能优化等。最后,我们将通过一个代码示例来展示如何实现一个简单的安卓应用。让我们一起开始吧!
|
7月前
|
存储 Java 开发工具
Android开发的技术与开发流程
Android开发的技术与开发流程
403 1
|
4月前
|
移动开发 搜索推荐 Android开发
安卓与iOS开发:一场跨平台的技术角逐
在移动开发的广阔舞台上,两大主角——安卓和iOS,持续上演着激烈的技术角逐。本文将深入浅出地探讨这两个平台的开发环境、工具和未来趋势,旨在为开发者揭示跨平台开发的秘密,同时激发读者对技术进步的思考和对未来的期待。
|
4月前
|
安全 Android开发 Swift
安卓与iOS开发:平台差异与技术选择
【8月更文挑战第26天】 在移动应用开发的广阔天地中,安卓和iOS两大平台各占一方。本文旨在探索这两个系统在开发过程中的不同之处,并分析开发者如何根据项目需求选择合适的技术栈。通过深入浅出的对比,我们将揭示各自平台的优势与挑战,帮助开发者做出更明智的决策。
74 5
|
4月前
|
移动开发 开发工具 Android开发
探索安卓与iOS开发的差异:技术选择的影响
【8月更文挑战第17天】 在移动应用开发的广阔天地中,安卓和iOS两大平台各领风骚。本文通过比较这两个平台的编程语言、开发工具及市场策略,揭示了技术选择对开发者和产品成功的重要性。我们将从开发者的视角出发,深入探讨不同平台的技术特性及其对项目实施的具体影响,旨在为即将步入移动开发领域的新手提供一个清晰的指南,同时给予资深开发者新的思考角度。
52 3
|
4月前
|
编解码 Android开发 iOS开发
安卓与iOS开发:平台差异下的技术创新之路
在数字时代的浪潮中,移动应用开发如同两股潮流——安卓与iOS,各自携带着独特的技术生态和文化基因。本文将深入探讨这两大平台的开发环境、编程语言和工具的差异,以及它们如何塑造了不同的用户体验和技术趋势。通过比较分析,我们旨在揭示跨平台开发的可能性和挑战,同时探索未来技术创新的方向。让我们一起跟随代码的足迹,穿越安卓的开放草原和iOS的精密园林,发现那些隐藏在平台差异之下的创新机遇。
44 1