android开发技术,android开发详细教程

首页 > 经验 > 作者:YD1662022-11-01 21:14:34

下面我们选取 Kotlin 的几个典型特性,结合代码简单介绍下其优势。

4.2 简化函数声明

Kotlin 语法的简洁体现在很多地方,就比如函数声明的简化。

如下是一个包含条件语句的 Java 函数的写法:

String generateAnswerString(int count, int countThreshold) { if (count > countThreshold) { return "I have the answer."; } else { return "The answer eludes me."; } }

Java 支持三元运算符可以进一步简化。

String generateAnswerString(int count, int countThreshold) { return count > countThreshold ? "I have the answer." : "The answer eludes me."; }

Kotlin 的语法并不支持三元运算符,但可以做到同等的简化效果:

fun generateAnswerString(count: Int, countThreshold: Int): String { return if (count > countThreshold) "I have the answer." else "The answer eludes me." }

它同时还可以省略大括号和 return 关键字,采用赋值形式进一步简化。这样子的写法已经很接近于语言的日常表达,高级~

fun generateAnswerString(count: Int, countThreshold: Int): String = if (count > countThreshold) "I have the answer." else "The answer eludes me."

反编译 Class 之后发现其实际上仍采用的三元运算符的写法,这种语法糖会体现在 Kotlin 的很多地方。

public final String generateAnswerString2(int count, int countThreshold) { return count > countThreshold ? "I have the answer." : "The answer eludes me."; }4.3 高阶函数

介绍高阶函数之前,我们先看一个向函数内传入回调接口的例子。

一般来说,需要先定义一个回调接口,调用函数传入接口实现的实例,函数进行一些处理之后执行回调,借助Lambda 表达式可以对接口的实现进行简化。

interface Mapper { int map(String input); } class Temp { void main() { stringMapper("Android", input -> input.length() 2); } int stringMapper(String input, Mapper mapper) { // Do something ... return mapper.map(input); } }

Kotlin 则无需定义接口,直接将匿名回调函数作为参数传入即可。(匿名函数是最后一个参数的话,方法体可单独拎出,增加可读性)

这种接受函数作为参数或返回值的函数称之为高阶函数,非常方便。

class Temp { fun main() { stringMapper("Android") {input -> input.length 2} } fun stringMapper(input: String, mapper: (String) -> Int): Int { // Do something ... return mapper(input) } }

事实上这也是语法糖,编译器会预设默认接口来帮忙实现高阶函数。

4.4 Null 安全

可以说 Null 安全是 Kotlin 语言的一大特色。试想一下 Java 传统的 Null 处理无非是在调用之前加上空判断或卫语句,这种写法既繁琐,更容易遗漏。

void Function(Bean bean) { // Null check if (bean != null) { bean.doSometh(); } // 或者卫语句 if (bean == null) { return; } bean.doSometh(); }

而 Kotlin 要求变量在定义的时候需要声明是否可为空:带上 ? 即表示可能为空,反之不为空。作为参数传递给函数的话也要保持是否为空的类型一致,否则无法通过编译。

比如下面的 functionA() 调用 functionB() 将导致编译失败,但 functionB() 的参数在声明的时候没有添加 ? 即为非空类型,那么函数内可直接使用该参数,没有 NPE 的风险。

fun functionA() { var bean: Bean? = null functionB(bean) } fun functionB(bean: Bean) { bean.doSometh() }

为了通过编译,可以将变量 bean 声明中的 ? 去掉, 并赋上正常的值。

但很多时候变量的值是不可控的,我们无法保证它不为空。那么为了通过编译,还可以选择将参数 bean 添加上 ? 的声明。这个时候函数内不就不可直接使用该参数了,需要做明确的 Null 处理,比如:

fun functionB(bean: Bean?) { // bean.doSometh() // 仍然直接调用将导致编译失败 // 不为空才调用 bean?.doSometh() // 或强制调用,开发者已知 NPE 风险 bean!!.doSometh() }

总结起来将很好理解:

反编译一段 Null 处理后可以看到,非空类型本质上是利用 @NotNull的注解,可空类型调用前的 ? 则是手动的 null 判断。

public final int stringMapper(@NotNull String str, @NotNull Function1 mapper) { ... return ((Number)mapper.invoke(str)).intValue(); } private final void function(String bean) { if (bean != null) { boolean var3 = false; Double.parseDouble(bean); } }4.5 协程 Coroutines

介绍 Coroutines 之前,先来回顾下 Java 或 Android 如何进行线程间通信?有何痛点?

android开发技术,android开发详细教程(13)

比如:AsyncTask、Handler、HandlerThread、IntentService、RxJava、LiveData 等。它们都有复杂易错、不简洁、回调冗余的痛点。

比如一个请求网络登录的简单场景:我们需要新建线程去请求,然后将结果通过 Handler 或 RxJava 回传给主线程,其中的登录请求必须明确写在非 UI 线程中。

void login(String username, String token) { String jsonBody = "{ username: \"$username\", token: \"$token\"}"; Executors.newSingleThreadExecutor().execute(() -> { Result result; try { result = makeLoginRequest(jsonBody); } catch (IOException e) { result = new Result(e); } Result finalResult = result; new Handler(Looper.getMainLooper()).post(() -> updateUI(finalResult)); }); } Result makeLoginRequest(String jsonBody) throws IOException { URL url = new URL("https://example.com/login"); HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection(); httpURLConnection.setRequestMethod("POST"); ... httpURLConnection.connect(); int code = httpURLConnection.getResponseCode(); if (code == 200) { // Handle input stream ... return new Result(bean); } else { return new Result(code); } }

Kotlin 的 Coroutines 则是以顺序的编码方式实现异步操作、同时不阻塞调用线程的简化并发处理的设计模式。

其具备如下的异步编程优势:

采用协程实现异步处理的将变得清晰、简洁,同时因为指定耗时逻辑运行在工作线程的缘故,无需管理线程切换可直接更新 UI。

fun login(username: String, token: String) { val jsonBody = "{ username: \"\$username\", token: \"\$token\"}" GlobalScope.launch(Dispatchers.Main) { val result = try { makeLoginRequest(jsonBody) } catch(e: Exception) { Result(e) } updateUI(result) } } @Throws(IOException::class) suspend fun makeLoginRequest(jsonBody: String): Result { val url = URL("https://example.com/login") var result: Result withContext(Dispatchers.IO) { val httpURLConnection = url.openConnection() as HttpURLConnection httpURLConnection.run { requestMethod = "POST" ... } httpURLConnection.connect() val code = httpURLConnection.responseCode result = if (code == 200) { Result(bean) } else { Result(code) } } return result }4.6 KTX

KTX 是专门为 Android 库设计的 Kotlin 扩展程序,以提供简洁易用的 Kotlin 代码。

比如使用 SharedPreferences 写入数据的话,我们会这么编码:

void updatePref(SharedPreferences sharedPreferences, boolean value) { sharedPreferences .edit() .putBoolean("key", value) .apply(); }

引入 KTX 扩展函数之后将变得更加简洁。

fun updatePref(sharedPreferences: SharedPreferences, value: Boolean) { sharedPreferences.edit { putBoolean("key", value) }

这只是 KTX 扩展的冰山一角,还有大量好用的扩展以及 Kotlin 的优势值得大家学习和实践,比如:

五、Jetpack

android开发技术,android开发详细教程(14)

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

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

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

android开发技术,android开发详细教程(15)

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

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

5.1 View Binding

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

通常做法缺点findViewById()NPE 风险、大量的绑定代码、类型转换危险@ButterKnifeNPE 风险、额外的注解代码、不适用于多模块项目(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 等。

优势局限Null 安全:预设 ID 的 View 才会被缓存,否则无法通过 ViewBinding 使用,在编译阶段就阻止了 NPE 的可能绑定布局以外的 View 仍需借助 findViewById类型安全:ViewBinding 缓存 View 实例的时候已经处理了匹配的类型依赖配置采用不同布局仍需处理 Null(比如横竖屏的布局不同)代码简洁:无需绑定的样板代码
布局专属:不混乱、布局文件为单位的专属类

5.2 Data Binding

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

  1. 创建 UI 布局
  2. 绑定布局中 View 实例
  3. 数据逐一更新到 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 还有很多优势:

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

5.5 Room

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

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

android开发技术,android开发详细教程(16)

上一页12345下一页

栏目热文

文档排行

本站推荐

Copyright © 2018 - 2021 www.yd166.com., All Rights Reserved.