diff --git a/Android/README.md b/Android/README.md index 1c03c119..2e8bcfc1 100644 --- a/Android/README.md +++ b/Android/README.md @@ -117,9 +117,11 @@ * js 通信 * 优化 * View + * [WebView 遇到的 Exception] + * [EditText 的 imeOptions 与多行输入的问题] + * [getDimensionxxx方法区别] * Snackbars * [Snackbars 常见问题] - * Materal Design * Toolbar * 沉浸式状态栏 diff --git "a/Android/adaptation/\345\261\217\345\271\225\351\200\202\351\205\215.md" "b/Android/adaptation/\345\261\217\345\271\225\351\200\202\351\205\215.md" index ed833c63..8474a73a 100644 --- "a/Android/adaptation/\345\261\217\345\271\225\351\200\202\351\205\215.md" +++ "b/Android/adaptation/\345\261\217\345\271\225\351\200\202\351\205\215.md" @@ -2,8 +2,6 @@   屏幕适配 - - ## 参考文章 [Android 屏幕适配全攻略](https://blog.csdn.net/zhaokaiqiang1992/article/details/45419023) diff --git a/Android/fragment/README.md b/Android/fragment/README.md index 39027335..6548220f 100644 --- a/Android/fragment/README.md +++ b/Android/fragment/README.md @@ -3,3 +3,15 @@ * [Fragment 的使用](https://github.com/ZhangMiao147/android_learning_notes/blob/master/Android/fragment/Fragment的使用.md) (添加 add 与 replace 的区别知识 20220408) + +* BottomSheetDialogFragment + * https://www.apiref.com/android-zh/android/support/design/widget/BottomSheetDialogFragment.html BootSheetDialogFragment API 参考文档 + * https://blog.csdn.net/zhaoyanjun6/article/details/127967304 Android BottomSheetDialogFragment 使用详解,设置圆角、固定高度、默认全屏等 + * https://juejin.cn/post/7111350570140565512 Android基础-BottomSheetDialog使用 + * https://www.jianshu.com/p/89c86880c4b6 Android DialogFragment、BottomSheetDialogFragment的基本使用 + * https://www.jianshu.com/p/27c768c6272e android BottomSheetDialogFragment 详解 + * https://www.freesion.com/article/2790419664/ ANDROID BOTTOMSHEETDIALOGFRAGMENT 可随手势滑动关闭的底部弹窗 + * https://www.jianshu.com/p/267f4bb0a3e4 【Android】BottomSheetDialogFragment使用笔记 +* DialogFragment + * https://www.jianshu.com/p/89c86880c4b6 Android DialogFragment、BottomSheetDialogFragment的基本使用 + * https://blog.csdn.net/u011272795/article/details/102822028 Android 弹窗 DialogFragment diff --git "a/Android/other/\346\225\243\345\210\227\347\256\227\346\263\225\346\257\224\350\276\203.md" "b/Android/other/\346\225\243\345\210\227\347\256\227\346\263\225\346\257\224\350\276\203.md" new file mode 100644 index 00000000..9acac2f6 --- /dev/null +++ "b/Android/other/\346\225\243\345\210\227\347\256\227\346\263\225\346\257\224\350\276\203.md" @@ -0,0 +1,97 @@ +# 散列算法比较:MD5、SHA1、SHA256 有哪些区别 + +## 算法比较 + +MD5、SHA1、SHA256 算法都属于散列算法,或者叫做哈希算法。它们具有输入任意长度,输出长度固定,以及单向性(无法根据散列值还原出消息)的特点。 + +**关于 MD5** + +MD5 是一个安全散列算法,输入两个不同的明文不会得到相同的输出值,根据输出值,不能得到原始的明文,即其过程是不可逆的。所以要解密 MD5 没有现成的算法,只能穷举法,把可能出现的明文,用 MD5 算法散列之后,得到的散列值和原始的数据形成一个一对一的映射表,通过匹配从映射表中找到破解密码所对应的原始明文。 + +**关于 SHA1** + +SHA1 是一个密码散列函数,可以生成一个被称为消息摘要的 160 位(20 字节)散列值,散列值通常的呈现形式为 40 个十六进制数。该算法输入报文的长度不限,产出的输出是一个 160 位的报文摘要。输入要按 512 位的分组进行处理的。SHA1 是不可逆的、防冲突,并具有良好的雪崩效应。 + +**关于 SHA256** + +SHA256 是一个密码散列函数,也可以说是哈希函数。对于任意长度的消息,SHA256 都会产生一个 256bit 长度的散列值,成为消息摘要,可以用一个长度为 64 的十六进制字符串表示。SHA256 是 SHA-2 下细分出的一个钟算法。SHA-2 下又可再分为六个不同的算法标准,包括了:SHA-224、SHA-256、SHA-384、SHA-512、SHA-512/224、SHA-512/256. + +**MD5、SHA1、SHA256 有哪些区别** + +相同点: + +都是密码散列函数,加密不可逆; + +都可以实现对任何长度对象加密,都不能防止碰撞; + +不同点: + +1. 校验值的长度不同,MD5 校验位的长度是 16 个字节(128 位);SHA1 是 20 个字节(160 位);SHA256 是 32 个字节(256 位) + +2. 运行速度不同,SHA256 的运行速度最慢,然后是 SHA1,最后是 MD5。 + +MD5、SHA1、SHA256 安全性如何? + +在安全性方面,SHA256 的安全性最高,然后是 SHA1,最后是 MD5。虽然 SHA256 的安全性比较高,但是耗时要比其他两种多很多。 + +**MD5、SHA1、SHA256 不能解密吗** + +SHA256 是目前比较流行的计算机算法之一,相对 MD5 和 SHA1 而言,SHA256 很安全。 + +SHA256 是牢不可破的函数,它的 256 位迷药从未泄漏过。而 MD5 就不一样了,单纯使用比较容易遭到撞库攻击。通过预先计算知道 MD5 的对应关系,存在数据库中,然后使用的时候反查,MD5 就可能被解密。 + +在网络安全实训中,也会用到这种相应的数据库进行查询。目前网上有很多 MD5 解密网站(md5.cn),可以通过秘闻查询到相应的口令,从而达到 “ 解密 ” 的目的,有一定的成功率。 + +在计算机安全领域,这些算法得到广泛应用。以上就是 MD5、SHA1、SHA256 的区别,可以灵活地选用这些算法达到实际目的。 + +## kotlin 使用算法加密 + +```kotlin +// 消息摘要,不可逆 +object MessageDigetUtil { + // 用户登录用的比较广泛 + fun md5(input: String):String { + val digest = MessageDigest.getInstance("MD5") + val result = digest.digest(input.toByteArray()) + return toHex(result) + } + + fun sha1(input: String):String { + val digest = MessageDigest.getInstance("SHA-1") + val result = digest.digest(input.toByteArray()) + return toHex(result) + } + + fun sha256(input: String):String { + val digest = MessageDigest.getInstance("SHA-256") + val result = digest.digest(input.toByteArray()) + return toHex(result) + } + + // 转成 16 进制 + fun toHex(byteArray: ByteArray): String { + // 转成 16 进制 + val result = with(StringBuilder()) { + byteArray.forEach { + val value = it + val hex = value.toInt() and (0xFF) + val hexStr = Integer.toHexString(hex) + if(hexStr.length == 1){ + append("0").append(hexStr) + } else { + append(hexStr) + } + } + this.toString + } + return result + } +} +``` + + + +## 参考文章 + +1. [散列算法比较:MD5、SHA1、SHA256有哪些区别 - 简书](https://www.jianshu.com/p/72d4f03aa3cb) +2. [kotlin 不可逆加密_kotlin sha256加密-CSDN博客](https://blog.csdn.net/qq_35698774/article/details/79002391) \ No newline at end of file diff --git "a/Android/sdk/Android SDK\345\274\200\345\217\221.md" "b/Android/sdk/Android SDK\345\274\200\345\217\221.md" new file mode 100644 index 00000000..e7b7711c --- /dev/null +++ "b/Android/sdk/Android SDK\345\274\200\345\217\221.md" @@ -0,0 +1,11 @@ +# Android SDK 开发艺术探索 + + + + + + + +## 参考文章 + +1. [Android SDK开发艺术探索(一)开篇与设计Android SDK开发艺术探索系列基于实际生产中的业务型SDK开发 - 掘金](https://juejin.cn/post/6864723217831952392?from=search-suggest) diff --git a/Android/view/README.md b/Android/view/README.md index 02089175..7810a919 100644 --- a/Android/view/README.md +++ b/Android/view/README.md @@ -13,7 +13,10 @@ ## View +* [WebView 遇到的 Exception] +* [EditText 的 imeOptions 与多行输入的问题] * BottomNavigationView http://www.45fan.com/article.php?aid=1COkLHKOUF2TiLJM +* [getDimensionxxx方法区别] ### Snackbars diff --git "a/Android/view/view/EditText\347\232\204imeOptions\344\270\216\345\244\232\350\241\214\350\276\223\345\205\245\347\232\204\351\227\256\351\242\230.md" "b/Android/view/view/EditText\347\232\204imeOptions\344\270\216\345\244\232\350\241\214\350\276\223\345\205\245\347\232\204\351\227\256\351\242\230.md" new file mode 100644 index 00000000..edd206b4 --- /dev/null +++ "b/Android/view/view/EditText\347\232\204imeOptions\344\270\216\345\244\232\350\241\214\350\276\223\345\205\245\347\232\204\351\227\256\351\242\230.md" @@ -0,0 +1,65 @@ +# EditText 的 imeOptions 与多行输入的问题 + +在 xml 为 EditText 中设置 imeOptions 可以控制键盘确认键的具体功能,如下列举了一些: + +```xml +android:imeOptions="flagNoExtractUi" //使软键盘不全屏显示,只占用一部分屏幕 同时, +这个属性还能控件软键盘右下角按键的显示内容,默认情况下为回车键 +android:imeOptions="actionNone" //输入框右侧不带任何提示 +android:imeOptions="actionGo" //右下角按键内容为'开始' +android:imeOptions="actionSearch" //右下角按键为放大镜图片,搜索 +android:imeOptions="actionSend" //右下角按键内容为'发送' +android:imeOptions="actionNext" //右下角按键内容为'下一步' 或者下一项 +android:imeOptions="actionDone" //右下角按键内容为'完成' +``` + +**问题描述:**因为 EditText 一旦设置了多行显示,键盘总是显示 Enter 键。有时候不仅需要文本输入多行显示,而且 Enter 键需要支持 imeOptions 设置,比如显示完成键而不是回撤换行。 + +当 EditText 弹出输入法时,会调用方法 public InputConnection onCreateInputConnection(EditorInfo outAttrs) 来创建和输入法的连接,设置输入法的状态,包括显示什么样的键盘布局。需要注意的地方时这部分的代码: + +```java +if (isMultilineInputType(outAttrs.inputType)) { + // Multi-line text editors should always show an enter key. + outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION; + } + + +private static boolean isMultilineInputType(int type) { + return (type & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE)) == + (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE); + } +``` + +发现,当 EditText 的 inputType 包含 textMultiLine 标志位,会强迫 imeOptions 加上 IME_FLAG_NO_ENTER_ACTION 位,这导致了只显示 Enter 键。 + +在网上找的解决方案是:可以继承 EditText 类,覆写 onCreateInputConnection 方法,如下: + +```java +@Override +public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + InputConnection connection = super.onCreateInputConnection(outAttrs); + int imeActions = outAttrs.imeOptions&EditorInfo.IME_MASK_ACTION; + if ((imeActions&EditorInfo.IME_ACTION_DONE) != 0) { + // clear the existing action + outAttrs.imeOptions ^= imeActions; + // set the DONE action + outAttrs.imeOptions |= EditorInfo.IME_ACTION_DONE; + } + if ((outAttrs.imeOptions&EditorInfo.IME_FLAG_NO_ENTER_ACTION) != 0) { + outAttrs.imeOptions &= ~EditorInfo.IME_FLAG_NO_ENTER_ACTION; + } + return connection; +} +``` + +然后还有一个坑,在基础 EditText 后,要重写完所有的构造函数,要不在 inflate 时会出错,直接调用父类的相关的构造方法就好。 + + + +**网上的解决方案试了是没有效果的,所以多行输入与控制键的设置问题解决不了。** + + + +## 参考文章 + +[EditText android:imeOptions与inputType="textMultiLine" 的坑](https://blog.csdn.net/a641324093/article/details/62238385) diff --git a/Android/view/view/README.md b/Android/view/view/README.md index ab325c7c..9888fcf1 100644 --- a/Android/view/view/README.md +++ b/Android/view/view/README.md @@ -1,5 +1,17 @@ # View +* [WebView 遇到的 Exception] +* [EditText 的 imeOptions 与多行输入的问题] +* [getDimensionxxx方法区别] + ## Snackbars -* [Snackbars 常见问题] \ No newline at end of file +* [Snackbars 常见问题] + + + + + +## 知识收集 + +* https://www.jianshu.com/p/9a7e518bfbea 关于TextView autolink的点击拦截和字体样式更改问题 \ No newline at end of file diff --git "a/Android/view/view/WebView\351\201\207\345\210\260\347\232\204Exception .md" "b/Android/view/view/WebView\351\201\207\345\210\260\347\232\204Exception .md" new file mode 100644 index 00000000..1b9228c0 --- /dev/null +++ "b/Android/view/view/WebView\351\201\207\345\210\260\347\232\204Exception .md" @@ -0,0 +1,97 @@ +# Web 遇到的 Exception + +## MissingWebViewPackageException + +遇到的 exception 为 : + +``` +android.webkit.WebViewFactory$MissingWebViewPackageException + +Failed to load WebView provider: No WebView installed +``` + +详细信息如下: + +``` +Caused by android.webkit.WebViewFactory$MissingWebViewPackageException: Failed to load WebView provider: No WebView installed + at android.webkit.WebViewFactory.getWebViewContextAndSetProvider(WebViewFactory.java:428) + at android.webkit.WebViewFactory.getProviderClass(WebViewFactory.java:493) + at android.webkit.WebViewFactory.getProvider(WebViewFactory.java:348) + at android.webkit.WebView.getFactory(WebView.java:2596) + at android.webkit.WebView.ensureProviderCreated(WebView.java:2590) + at android.webkit.WebView.setOverScrollMode(WebView.java:2677) + at android.view.View.(View.java:5567) + at android.view.View.(View.java:5742) + at android.view.ViewGroup.(ViewGroup.java:720) + at android.widget.AbsoluteLayout.(AbsoluteLayout.java:58) + at android.webkit.WebView.(WebView.java:423) + at android.webkit.WebView.(WebView.java:365) + at android.webkit.WebView.(WebView.java:347) + at android.webkit.WebView.(WebView.java:334) + at android.webkit.WebView.(WebView.java:324) + at packagename.EnterFragmentKt$EnterScreen$5$1$1$3$1.invoke(:7) + at packagename.EnterFragmentKt$EnterScreen$5$1$1$3$1.invoke(:2) + at androidx.compose.ui.viewinterop.ViewFactoryHolder.setFactory(ViewFactoryHolder.java:13) + at androidx.compose.ui.viewinterop.AndroidView_androidKt$AndroidView$1.invoke(:13) + at androidx.compose.ui.viewinterop.AndroidView_androidKt$AndroidView$1.invoke() + at androidx.compose.ui.viewinterop.AndroidView_androidKt$AndroidView$$inlined$ComposeNode$1.invoke(:2) + at androidx.compose.runtime.ComposerImpl$createNode$2.invoke(ComposerImpl.java:2) + at androidx.compose.runtime.ComposerImpl$createNode$2.invoke(ComposerImpl.java:6) + at androidx.compose.runtime.ComposerImpl$recordInsert$2.invoke(ComposerImpl.java:2) + at androidx.compose.runtime.ComposerImpl$recordInsert$2.invoke(ComposerImpl.java:6) + at androidx.compose.runtime.CompositionImpl.applyChangesInLocked(CompositionImpl.java:60) + at androidx.compose.runtime.CompositionImpl.applyChanges(CompositionImpl.java:5) + at androidx.compose.runtime.Recomposer.composeInitial$runtime_release(:86) + at androidx.compose.runtime.CompositionImpl.setContent(CompositionImpl.java:15) + at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(:89) + at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(:2) + at androidx.compose.ui.platform.AndroidComposeView.setOnViewTreeOwnersAvailable(AndroidComposeView.java:11) + at androidx.compose.ui.platform.WrappedComposition.setContent(:12) + at androidx.compose.ui.platform.WrappedComposition.onStateChanged(:29) + at androidx.lifecycle.LifecycleRegistry$ObserverWithState.dispatchEvent(:14) + at androidx.lifecycle.LifecycleRegistry.forwardPass(LifecycleRegistry.java:69) + at androidx.lifecycle.LifecycleRegistry.sync(LifecycleRegistry.java:72) + at androidx.lifecycle.LifecycleRegistry.moveToState(LifecycleRegistry.java:2) + at androidx.lifecycle.LifecycleRegistry.handleLifecycleEvent(LifecycleRegistry.java:9) + at androidx.fragment.app.FragmentViewLifecycleOwner.handleLifecycleEvent(FragmentViewLifecycleOwner.java:2) + at androidx.fragment.app.Fragment.restoreViewState(Fragment.java:43) + at androidx.fragment.app.Fragment.restoreViewState(Fragment.java:35) + at androidx.fragment.app.Fragment.performActivityCreated(Fragment.java:18) + at androidx.fragment.app.FragmentStateManager.activityCreated(FragmentStateManager.java:2) + at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:2) + at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:230) + at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:91) + at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:21) + at androidx.fragment.app.FragmentManager$4.run(:3) + at android.os.Handler.handleCallback(Handler.java:938) + at android.os.Handler.dispatchMessage(Handler.java:99) + at android.os.Looper.loopOnce(Looper.java:226) + at android.os.Looper.loop(Looper.java:313) + at android.app.ActivityThread.main(ActivityThread.java:8751) + at java.lang.reflect.Method.invoke(Method.java) + at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:571) + at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1135) +``` + +解决方法是: + +```java +public class MyWebView extends WebView { + + @Override + public void setOverScrollMode(int mode) { + try { + super.setOverScrollMode(mode); + } catch (Exception e) { + if (e.getMessage() != null && e.getMessage().contains("Failed to load WebView provider: No WebView installed")) { + e.printStackTrace(); + } else { + throw e; + } + } + } +} +``` + + + diff --git "a/Android/view/view/getDimensionxxx\346\226\271\346\263\225\345\214\272\345\210\253.md" "b/Android/view/view/getDimensionxxx\346\226\271\346\263\225\345\214\272\345\210\253.md" new file mode 100644 index 00000000..3916f66f --- /dev/null +++ "b/Android/view/view/getDimensionxxx\346\226\271\346\263\225\345\214\272\345\210\253.md" @@ -0,0 +1,22 @@ +# getDimension()、getDimensionPixelOffset() 和 getDimensionPixelSize() 区别 + +在自定义控件中使用自定义属性时,经常需要使用 java 代码获取在 xml 中定义的尺寸,相关有以下三个函数: + +* getDimension() +* getDimensionPixelOffset() +* getDimensionPixelSize() + +在类 TypedArray 和类 Resources 中都有这三个函数,功能类似,TypeArray 中的函数是获取自定义属性的,Resources 中的函数是获取 android 预置属性的。 + +API reference 里的解释: + +* getDimension() 是基于当前 DisplayMetrics 进行转换,获取指定资源 id 对应的尺寸。文档里并没说这里返回的就是像素,要注意这个函数的返回值是 float,像素肯定是 int。 +* getDimensionPixelSize() 与 getDimension() 功能类似,不同的是将结果转换为 int,并且小数部分四舍五入。 +* getDimensionPixelOffset() 与 getDimension() 功能类似,不同的是将结果转换为 int,并且偏移转换(offset conversion,函数命名中的 offset 是这个意思)是直接截断小数位,即取整(其实就是把 float 强制转换为 int,注意不是四舍五入哦)。 + +由此可见,这三个函数返回的都是绝对尺寸,而不是相对尺寸(dp/sp等)。如果 getDimension() 返回结果是 20.5f,那么 getDimensionPixelSize() 返回结果就是 21,getDimensionPixelOffset() 返回结果就是 20。 + +# 参考文章 + +1. [【android】getDimension()、getDimensionPixelOffset()和getDimensionPixelSize()区别详解](https://www.cnblogs.com/ldq2016/p/6834959.html) + diff --git "a/Android/view/view\347\232\204\345\256\236\350\267\265/README.md" "b/Android/view/view\347\232\204\345\256\236\350\267\265/README.md" index 581a52c4..1f6489fd 100644 --- "a/Android/view/view\347\232\204\345\256\236\350\267\265/README.md" +++ "b/Android/view/view\347\232\204\345\256\236\350\267\265/README.md" @@ -1,5 +1,4 @@ # View 实践 * [Android实现卡片翻转的动画(翻牌动画)](https://github.com/ZhangMiao147/android_learning_notes/blob/master/Android/view/view%E7%9A%84%E5%AE%9E%E8%B7%B5/Android%E5%AE%9E%E7%8E%B0%E5%8D%A1%E7%89%87%E7%BF%BB%E8%BD%AC%E7%9A%84%E5%8A%A8%E7%94%BB%EF%BC%88%E7%BF%BB%E7%89%8C%E5%8A%A8%E7%94%BB%EF%BC%89.md) - * [系统控件的常用使用](https://github.com/ZhangMiao147/android_learning_notes/blob/master/Android/view/view%E7%9A%84%E5%AE%9E%E8%B7%B5/%E7%B3%BB%E7%BB%9F%E6%8E%A7%E4%BB%B6%E7%9A%84%E5%B8%B8%E7%94%A8%E4%BD%BF%E7%94%A8.md) \ No newline at end of file diff --git "a/Android/Android\345\270\270\350\247\201\351\227\256\351\242\2301.md" "b/Android/\351\235\242\350\257\225/Android\345\270\270\350\247\201\351\227\256\351\242\2301.md" similarity index 100% rename from "Android/Android\345\270\270\350\247\201\351\227\256\351\242\2301.md" rename to "Android/\351\235\242\350\257\225/Android\345\270\270\350\247\201\351\227\256\351\242\2301.md" diff --git "a/Android/Android\345\270\270\350\247\201\351\227\256\351\242\2302.md" "b/Android/\351\235\242\350\257\225/Android\345\270\270\350\247\201\351\227\256\351\242\2302.md" similarity index 100% rename from "Android/Android\345\270\270\350\247\201\351\227\256\351\242\2302.md" rename to "Android/\351\235\242\350\257\225/Android\345\270\270\350\247\201\351\227\256\351\242\2302.md" diff --git "a/Android/Android\345\270\270\350\247\201\351\227\256\351\242\2303.md" "b/Android/\351\235\242\350\257\225/Android\345\270\270\350\247\201\351\227\256\351\242\2303.md" similarity index 100% rename from "Android/Android\345\270\270\350\247\201\351\227\256\351\242\2303.md" rename to "Android/\351\235\242\350\257\225/Android\345\270\270\350\247\201\351\227\256\351\242\2303.md" diff --git "a/Android/Android\345\270\270\350\247\201\351\227\256\351\242\2304.md" "b/Android/\351\235\242\350\257\225/Android\345\270\270\350\247\201\351\227\256\351\242\2304.md" similarity index 100% rename from "Android/Android\345\270\270\350\247\201\351\227\256\351\242\2304.md" rename to "Android/\351\235\242\350\257\225/Android\345\270\270\350\247\201\351\227\256\351\242\2304.md" diff --git "a/Android/Android\345\270\270\350\247\201\351\227\256\351\242\2305.md" "b/Android/\351\235\242\350\257\225/Android\345\270\270\350\247\201\351\227\256\351\242\2305.md" similarity index 100% rename from "Android/Android\345\270\270\350\247\201\351\227\256\351\242\2305.md" rename to "Android/\351\235\242\350\257\225/Android\345\270\270\350\247\201\351\227\256\351\242\2305.md" diff --git "a/Android/Android\345\270\270\350\247\201\351\227\256\351\242\2306.md" "b/Android/\351\235\242\350\257\225/Android\345\270\270\350\247\201\351\227\256\351\242\2306.md" similarity index 100% rename from "Android/Android\345\270\270\350\247\201\351\227\256\351\242\2306.md" rename to "Android/\351\235\242\350\257\225/Android\345\270\270\350\247\201\351\227\256\351\242\2306.md" diff --git "a/Android/Android\345\270\270\350\247\201\351\227\256\351\242\2307.md" "b/Android/\351\235\242\350\257\225/Android\345\270\270\350\247\201\351\227\256\351\242\2307.md" similarity index 100% rename from "Android/Android\345\270\270\350\247\201\351\227\256\351\242\2307.md" rename to "Android/\351\235\242\350\257\225/Android\345\270\270\350\247\201\351\227\256\351\242\2307.md" diff --git "a/Android/Android\345\270\270\350\247\201\351\227\256\351\242\2308.md" "b/Android/\351\235\242\350\257\225/Android\345\270\270\350\247\201\351\227\256\351\242\2308.md" similarity index 100% rename from "Android/Android\345\270\270\350\247\201\351\227\256\351\242\2308.md" rename to "Android/\351\235\242\350\257\225/Android\345\270\270\350\247\201\351\227\256\351\242\2308.md" diff --git "a/Android/Android\345\270\270\350\247\201\351\227\256\351\242\2309.md" "b/Android/\351\235\242\350\257\225/Android\345\270\270\350\247\201\351\227\256\351\242\2309.md" similarity index 100% rename from "Android/Android\345\270\270\350\247\201\351\227\256\351\242\2309.md" rename to "Android/\351\235\242\350\257\225/Android\345\270\270\350\247\201\351\227\256\351\242\2309.md" diff --git "a/Android/Android\345\270\270\350\247\201\351\227\256\351\242\230\347\233\256\345\275\225.md" "b/Android/\351\235\242\350\257\225/Android\345\270\270\350\247\201\351\227\256\351\242\230\347\233\256\345\275\225.md" similarity index 100% rename from "Android/Android\345\270\270\350\247\201\351\227\256\351\242\230\347\233\256\345\275\225.md" rename to "Android/\351\235\242\350\257\225/Android\345\270\270\350\247\201\351\227\256\351\242\230\347\233\256\345\275\225.md" diff --git "a/Android/\343\200\220\351\273\221\351\251\254\351\235\242\350\257\225\345\256\235\345\205\270\343\200\221\350\231\220\351\235\242\350\257\225\345\256\230\347\245\236\345\231\250\344\271\213\345\277\205\345\244\207\351\273\221\351\251\254\347\250\213\345\272\217\345\221\230\351\235\242\350\257\225\345\256\235\345\205\270V2.5.pdf" "b/Android/\351\235\242\350\257\225/\343\200\220\351\273\221\351\251\254\351\235\242\350\257\225\345\256\235\345\205\270\343\200\221\350\231\220\351\235\242\350\257\225\345\256\230\347\245\236\345\231\250\344\271\213\345\277\205\345\244\207\351\273\221\351\251\254\347\250\213\345\272\217\345\221\230\351\235\242\350\257\225\345\256\235\345\205\270V2.5.pdf" similarity index 100% rename from "Android/\343\200\220\351\273\221\351\251\254\351\235\242\350\257\225\345\256\235\345\205\270\343\200\221\350\231\220\351\235\242\350\257\225\345\256\230\347\245\236\345\231\250\344\271\213\345\277\205\345\244\207\351\273\221\351\251\254\347\250\213\345\272\217\345\221\230\351\235\242\350\257\225\345\256\235\345\205\270V2.5.pdf" rename to "Android/\351\235\242\350\257\225/\343\200\220\351\273\221\351\251\254\351\235\242\350\257\225\345\256\235\345\205\270\343\200\221\350\231\220\351\235\242\350\257\225\345\256\230\347\245\236\345\231\250\344\271\213\345\277\205\345\244\207\351\273\221\351\251\254\347\250\213\345\272\217\345\221\230\351\235\242\350\257\225\345\256\235\345\205\270V2.5.pdf" diff --git "a/Android/\351\237\263\350\247\206\351\242\221/README.md" "b/Android/\351\237\263\350\247\206\351\242\221/README.md" index e22ca1f6..542248db 100644 --- "a/Android/\351\237\263\350\247\206\351\242\221/README.md" +++ "b/Android/\351\237\263\350\247\206\351\242\221/README.md" @@ -1,5 +1,9 @@ # 音视频 +1.1.mac 账号:1006299425 密码:181501,播放器下载地址:http://www.drmsoft.net/playernet/down.asp + +1.2.链接:https://pan.baidu.com/s/1eN2BdtnwW17VTu4e7EXT0A?pwd=d0sx 提取码:d0sx + ## C 语言基础 ### C 语言初探 diff --git "a/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/README.md" "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/README.md" new file mode 100644 index 00000000..33153e66 --- /dev/null +++ "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/README.md" @@ -0,0 +1,32 @@ +# Android Jetpack 应用指南 + +**第 1 章** [初识 Jetpack] + +**第 2 章** [LifeCycle] + +**第 3 章** [Navigation] + +**第 4 章** [ViewModel] + +**第 5 章** [LiveData] + +**第 6 章** [Room] + +**第 7 章** [WorkManager] + +**第 8 章** [DataBinding] + +**第 9 章** [Paging] + +**第 10 章** [MVVM 架构] + + + + + + + + + + + diff --git "a/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/BoundaryCallback\344\275\277\347\224\250\346\265\201\347\250\213.png" "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/BoundaryCallback\344\275\277\347\224\250\346\265\201\347\250\213.png" new file mode 100644 index 00000000..a49402e1 Binary files /dev/null and "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/BoundaryCallback\344\275\277\347\224\250\346\265\201\347\250\213.png" differ diff --git "a/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/BoundaryCallback\351\241\271\347\233\256\346\236\266\346\236\204.png" "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/BoundaryCallback\351\241\271\347\233\256\346\236\266\346\236\204.png" new file mode 100644 index 00000000..367a8d47 Binary files /dev/null and "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/BoundaryCallback\351\241\271\347\233\256\346\236\266\346\236\204.png" differ diff --git "a/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/ItemKeyeddataSource\351\241\271\347\233\256\346\236\266\346\236\204.png" "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/ItemKeyeddataSource\351\241\271\347\233\256\346\236\266\346\236\204.png" new file mode 100644 index 00000000..8e07fe98 Binary files /dev/null and "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/ItemKeyeddataSource\351\241\271\347\233\256\346\236\266\346\236\204.png" differ diff --git "a/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/MVVM\346\236\266\346\236\204.png" "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/MVVM\346\236\266\346\236\204.png" new file mode 100644 index 00000000..35f4f8ba Binary files /dev/null and "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/MVVM\346\236\266\346\236\204.png" differ diff --git "a/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/MVVM\351\241\271\347\233\256\347\273\223\346\236\204.png" "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/MVVM\351\241\271\347\233\256\347\273\223\346\236\204.png" new file mode 100644 index 00000000..19bb06f3 Binary files /dev/null and "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/MVVM\351\241\271\347\233\256\347\273\223\346\236\204.png" differ diff --git "a/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/PageKeyedDataSource\351\241\271\347\233\256\346\236\266\346\236\204.png" "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/PageKeyedDataSource\351\241\271\347\233\256\346\236\266\346\236\204.png" new file mode 100644 index 00000000..0d16bb43 Binary files /dev/null and "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/PageKeyedDataSource\351\241\271\347\233\256\346\236\266\346\236\204.png" differ diff --git "a/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/Room\346\236\266\346\236\204.png" "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/Room\346\236\266\346\236\204.png" new file mode 100644 index 00000000..bdf63896 Binary files /dev/null and "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/Room\346\236\266\346\236\204.png" differ diff --git "a/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/databind\345\221\275\345\220\215.png" "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/databind\345\221\275\345\220\215.png" new file mode 100644 index 00000000..0e3615d4 Binary files /dev/null and "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/databind\345\221\275\345\220\215.png" differ diff --git "a/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/livedata.png" "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/livedata.png" new file mode 100644 index 00000000..5c98f779 Binary files /dev/null and "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/livedata.png" differ diff --git "a/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/livedata_fragment.png" "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/livedata_fragment.png" new file mode 100644 index 00000000..84cbf452 Binary files /dev/null and "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/livedata_fragment.png" differ diff --git "a/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/livedata\344\275\277\347\224\250.png" "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/livedata\344\275\277\347\224\250.png" new file mode 100644 index 00000000..01876151 Binary files /dev/null and "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/livedata\344\275\277\347\224\250.png" differ diff --git "a/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/navigation\346\213\226\346\213\275.png" "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/navigation\346\213\226\346\213\275.png" new file mode 100644 index 00000000..a3885e2c Binary files /dev/null and "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/navigation\346\213\226\346\213\275.png" differ diff --git "a/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/padingdemo.png" "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/padingdemo.png" new file mode 100644 index 00000000..2f381e7e Binary files /dev/null and "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/padingdemo.png" differ diff --git "a/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/paging\345\267\245\344\275\234\345\216\237\347\220\206.png" "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/paging\345\267\245\344\275\234\345\216\237\347\220\206.png" new file mode 100644 index 00000000..25a97b34 Binary files /dev/null and "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/paging\345\267\245\344\275\234\345\216\237\347\220\206.png" differ diff --git "a/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/paging\346\212\200\346\234\257.png" "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/paging\346\212\200\346\234\257.png" new file mode 100644 index 00000000..216e8ac3 Binary files /dev/null and "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/paging\346\212\200\346\234\257.png" differ diff --git "a/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/paging\346\224\257\346\214\201\346\236\266\346\236\204.png" "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/paging\346\224\257\346\214\201\346\236\266\346\236\204.png" new file mode 100644 index 00000000..a40ce25d Binary files /dev/null and "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/paging\346\224\257\346\214\201\346\236\266\346\236\204.png" differ diff --git "a/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/room_livedata.png" "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/room_livedata.png" new file mode 100644 index 00000000..b6884bed Binary files /dev/null and "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/room_livedata.png" differ diff --git "a/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/then.png" "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/then.png" new file mode 100644 index 00000000..268b2c17 Binary files /dev/null and "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/then.png" differ diff --git "a/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/viewmodel.png" "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/viewmodel.png" new file mode 100644 index 00000000..a99fc5a0 Binary files /dev/null and "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/viewmodel.png" differ diff --git "a/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/viewmodel\347\224\237\345\221\275\345\221\250\346\234\237.png" "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/viewmodel\347\224\237\345\221\275\345\221\250\346\234\237.png" new file mode 100644 index 00000000..797c8338 Binary files /dev/null and "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/viewmodel\347\224\237\345\221\275\345\221\250\346\234\237.png" differ diff --git "a/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/\345\215\225\345\220\221\347\273\221\345\256\232.png" "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/\345\215\225\345\220\221\347\273\221\345\256\232.png" new file mode 100644 index 00000000..686677fd Binary files /dev/null and "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/\345\215\225\345\220\221\347\273\221\345\256\232.png" differ diff --git "a/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/\345\217\214\345\220\221\347\273\221\345\256\232.png" "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/\345\217\214\345\220\221\347\273\221\345\256\232.png" new file mode 100644 index 00000000..be11a329 Binary files /dev/null and "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/\345\217\214\345\220\221\347\273\221\345\256\232.png" differ diff --git "a/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/\346\225\260\346\215\256\346\235\245\346\272\220.png" "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/\346\225\260\346\215\256\346\235\245\346\272\220.png" new file mode 100644 index 00000000..08606356 Binary files /dev/null and "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/img/\346\225\260\346\215\256\346\235\245\346\272\220.png" differ diff --git "a/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/\347\254\25410\347\253\240-MVVM\346\236\266\346\236\204.md" "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/\347\254\25410\347\253\240-MVVM\346\236\266\346\236\204.md" new file mode 100644 index 00000000..3a935b0d --- /dev/null +++ "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/\347\254\25410\347\253\240-MVVM\346\236\266\346\236\204.md" @@ -0,0 +1,516 @@ +# 第 10 章 MVVM 架构 + +### 10.1. MVVM 架构与 Jetpack + +MVVM 即 Model-View-ViewModel 的缩写。它的出现是为了将图形界面与业务逻辑、数据模型进行解耦。MVVM 也是 Google 推崇的一种 Android 项目架构模型。在前面的章节中所学习的 Jetpack 组件,大部分都是为了能够更好地架构 MVVM 应用程序而设计的。 + +### 10.2. 数据模型驱动界面更新 + +MVVM 架构的应用程序采用了数据模型驱动界面更新的设计方案。因此,在了解 MVVM 之前,需要先了解数据模型驱动界面更新的概念。希望数据在发生变化时,界面能够自动得到通知并进行更新,这就是数据模型启动界面更新。对于普通应用程序,数据的来源无非就两种,一种来源于本地,通常是本地数据库;另外一种来源于远程服务端,即网络数据。 + +![](img/数据来源.png) + +从图可以看出,ViewModel 曾的数据既可以来源于本地数据库,也可以来源于远程服务器。在实际开发过程中,为了更好的用户体验,通常会将本地数据库和远程服务器这两种方式进行结合。若在 ViewModel 层对本地数据和网络数据进行业务逻辑处理,势必会增加 ViewModel 的复杂度。因此,可以在 ViewModel 层和 Model 层之间引入 Repository 层。在 Repository 层处理本地数据和网络数据之间的业务逻辑,让 Repository 层对 ViewModel 层负责,使 ViewModel 只需要关心自己的业务逻辑,而不用关心数据的具体来源,这便是 Repository 层存在的意义。引用 Repository 层后的架构如下: + +![](img/MVVM架构.png) + +请永远记住,MVVM 架构中每一层的职责都是清晰的、明确的,且只对其上一层负责。这样从长远来看,随着代码量的增加,不容易让项目陷入技术债务中。并且由于每一层的职责单一,每个模块也更容易进行独立测试。 + +### 10.3. 简单回顾 Jetpack 组件 + +LifeCycle:有了 LifeCycle 组件,当系统组件 Activity、Fragment、Service 和 Application 的生命周期发生变化时,自定义组件能够及时得到通知。LifeCycle 使自定义组件与系统组件进一步解耦。 + +Navigation:处理导航图所需的一切,包括页面的跳转、参数的传递、动画效果的设置,以及 App bar 的设置等。导航图让我们可以站在 “上帝的视角”,俯瞰应用程序所有界面之间的关系。 + +ViewModel:顾名思义,ViewModel 负责处理和存放 View 与 Model 之间的业务逻辑。它直接对 UI 界面所需的数据负责,让视图和数据进行分离。并且,ViewModel 与生命周期相关,它能自动处理由于屏幕旋转导致界面重新创建所带来的数据重新获取问题。 + +LiveData:LiveData 在 MVVM 结构的层与层之间扮演者桥梁的作用。当数据发生变化时,通过 LiveData 让数据的订阅者得到通知。 + +Room:Google 官方的 ORM 数据库,原生支持 LiveData。在搭配 LiveData 使用时,当 Room 数据库中的数据发生变化,LiveData 使数据的订阅者能够及时得到通知,而无需从数据库重新获取数据。 + +WorkManager:为应用程序中那些不需要及时完成的任务提供同意的解决方案。 + +DataBinding:进一步解耦 UI 界面。DataBinding 的出现让 findViewById 不复存在,使布局文件能够承担更多的工作,甚至能承担一些简单的业务逻辑,这减轻了 Activity/Fragment 的工作量。 + +Paging:为常见的 3 种分页机制提供了统一的解决方案,使工程师能够将更多的精力专注在业务代码上。 + +### 10.4. 使用 Jetpack 组件构建 MVVM 应用程序 + +本节将用到的 Jetpack 组件有 LiveData、Room、ViewModel 和 DataBinding。对于演示如何构建一个简单的 MVVM 应用程序,这已经足够了。这里没有强行引入其他组件,例如 Navigation、Paging 等,这些组件有其特殊的用途,对于架构一个完整的应用程序是必要的。但在本节中,主要目的是演示什么是 MVVM、什么是数据模型驱动视图更新。力求通过最简单的案例将其解释清楚。 + +1. 案例分析 + + 假设需要通过 GitHub API 获取某个特定用户的个人信息并进行展示。 + +2. API 接口 + + ``` + api.github.com/users/michaelye + ``` + +3. 项目工程结构 + + ![](img/MVVM项目结构.png) + + 结合项目的工程结构图,可以大致将项目分为以下几个层级。 + + * Model:使用 Room 数据库存储用户个人信息。 + * API:通过 Retrofit 库请求 Github API。 + * Application:在其中实例化 Room 和 Retrofit 对象,以便统一管理和全局调用。 + * Repository:构建 Repository 层,用于处理 Room 中的数据与 API 接口的来的网络数据之间的业务关系。 + * ViewModel:从 Repository 层中获取数据,ViewModel 不需要关心数据的来源是 Room 还是 API 接口。 + * View:Activity 和布局文件,将会用到 DataBinding 组件。 + +4. 代码分析 + + a. 准备工作。 + + 由于需要勤秋网络数据,因此需要在 Manifest 文件中开启网络权限。 + + ``` + + ``` + + 在 app 的 build.gradle 文件中启用 DataBinding,并为所需组件添加相关依赖。注意,除了 Jetpack 组件,这里还用到了用于 API 请求的 Retrofit 库、用于图片加载的 Picasso 库、下拉刷新组件,以及用于生成圆形头像的组件 CircleImageView。 + + ```groovy + android { + ... + dataBinding{ + enabled = true + } + } + + dependencies { + ... + implementation 'androidx.lifecycle.lifecycle-extensions:2.2.0' + implementation 'com.squareup.retrofit2:retrofit:2.6.2' + implementation 'com.squareup.retrofit2:converter-gson:2.4.0' + implementation 'com.squareup.picasso:picasso:2.71828' + implementation "androidx.room:room-runtime:2.2.2" + annotationProcessor "andriodx.room:room-compiler:2.2.2" + implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0' + implementation 'de.hdodenhof:circleimageview:3.0.1' + } + ``` + + b. Model 层。 + + 首先需要定义 User 模型,该模型在整个应用程序中都需要被用到,包括数据库、API 接口和 View。模型中的字段来源于 GitHub API 接口所返回的数据,其中只保留所需要的字段。 + + ```java + @Entity(tableName = "user") + public class User{ + @PrimaryKey() + @ColumnInfo(name="id", typeAffinity = ColumnInfo.INTEGER) + public int id; + + @ColumnInfo(name="name", typeAffinity = ColumnInfo.TEXT) + public String name; + + @ColumnInfo(name="avatar", typeAffinity = ColumnInfo.TEXT) + @SerializedName("avatar_url") + public String avatar; + + @ColumnInfo(name="followers", typeAffinity = ColumnInfo.INTEGER) + public String followers; + + @ColumnInfo(name="following", typeAffinity = ColumnInfo.INTEGER) + public String following; + + @ColumnInfo(name="blog", typeAffinity = ColumnInfo.TEXT) + public String blog; + + @ColumnInfo(name="company", typeAffinity = ColumnInfo.TEXT) + public String company; + + @ColumnInfo(name="bio", typeAffinity = ColumnInfo.TEXT) + public String bio; + + @ColumnInfo(name="location", typeAffinity = ColumnInfo.TEXT) + public String location; + + @ColumnInfo(name="htmlUrl", typeAffinity = ColumnInfo.TEXT) + @SerializedName("html_url") + public String htmlUrl; + + public User(int id, String name, String avatar, int followers, + int following, String blog, String company, + String bio, String location, String htmlUrl){ + this.id = id; + this.name = name; + this.avatar = avatar; + this.followers = followers; + this.following = following; + this.blog = blog; + this.company = company; + this.bio = bio; + this.location = location; + this.htmlUrl = htmlUrl; + } + } + ``` + + 创建 Room 数据库。 + + ```java + @Database(entities = {User.class}, version = 1) + public abstract class UserDatabase extends RoomDatabase{ + private static final String DATABASE_NAME = "user_db"; + + private static UserDatabase databaseInstance; + + public static synchronized UserDatabase getInstance(Context context){ + if(databaseInstance == null){ + databaseInstance = Room + .databaseBuilder(context.getApplicationContext(), + UserDatabase.class, DATABASE_NAME).build(); + } + return databaseInstance; + + } + + public abstract UserDao userDao(); + } + ``` + + 创建用于访问 User 表的 Dao 文件。注意,在查询方法中返回的是 LiveData 包装过的 User,这样,当 Room 中的数据发生变化时,能够自动通知数据观察者。 + + ``` + @Dao + public interface UserDao{ + @Insert(onconflict = OnConflictStarategy.REPLACE) + void insertUser(User user); + + @Delete + void deleteStudent(User user); + + @Query("SELECT * FROM user WHERE name = :name") + LiveData getUserByName(String name); + } + ``` + + c. API 层。 + + 定义 Api 接口。 + + ```java + public interface Api{ + @GET("users/{userId}") + Call getUser( + @Path("userId") String userId + ); + } + ``` + + 实例化 Retrofit 对象。 + + ``` + public class RetrofitClient{ + private static final String BASE_URL = "https://***.github.com/"; + private static RetrofitClient retrofitClient; + private Retrofit retrofit; + + private RetrofitClient(){ + if(retrofitClient == null){ + retrofitClient = new RetrofitClient(); + } + return retrofitClient; + } + + public Api getApi(){ + return retrofit.create(Api.class); + } + } + ``` + + d. Application 层。 + + 既然已经定义好了 Room 和 Retrofit,便可以在 Application 中对其进行实例化了。这样做的好处在于,方便统一管理和全局调用这两个对象,并且不用担心这两个对象的生命周期问题,数据库和 Retrofit 对象通常伴随着这个应用程序的生命周期。因此,在 Application 中进行实例化是一个比较好的做法。 + + ``` + public class MyApplication extends Application{ + @Override + public void onCreate(){ + super.onCreate(); + userDatabase = UserDatabase.getInstance(this); + api = RetrofitClient.getInstance().getApi(); + } + + private static UserDatabase userDatabase; + private static Api api; + + public static Api getApi(){ + return api; + } + + public static UserDatabase getUserDatabase(){ + return userDatabase; + } + } + ``` + + e. Repository 层。 + + 在该层请求网络数据,并将得到的数据写入 Room 数据库。需要注意的是,该类只对 ViewModel 负责。它提供了两个方法 getUser() 和 refresh()。当 ViewModel 需要数据时,它不用关心数据是来自网络还是本地数据库。在 getUser() 方法中,数据直接来源于 Room 数据库。并且在每次调用该方法时,都会调用 refresh() 方法更新一次数据。refresh() 方法也被下拉刷新组件所调用。 + + 当网络数据请求成功后,会直接将其写入 Room 数据库。神奇的地方在于,由于使用了 LiveData,当数据有变化时,ViewModel 会自动得到通知,因此,不用担心数据更新问题。 + + ```java + public class UserRepository{ + private String TAG = this.getClass().getName(); + private UserDao userDao; + private Api api; + + public UserRepository(UserDao userDao, Api api){ + this.api = api; + this.userDao = userDao; + } + + public LiveData getUser(final String name){ + refresh(name); + return userDao.getUserByName(name); + } + + public void refresh(String userName){ + api.getUser(userName).enqueue(new Callback(){ + @Override + public void onResponse(Call call, + Response response){ + if(response.body() != null){ + insertUser(response.body()); + } + } + + @Override + public void onFailure(Call call, Throwable t){} + }); + } + + private void insertUser(final User user){ + AsyncTask.execute(new Runnable(){ + @Override + public void run(){ + userDao.insertUser(user); + } + }); + } + } + ``` + + f. ViewModel 层 + + 在 ViewModel 的构造器中,实例化 Repository 对象,并将数据库对象和 Retrofit 对象以构造器参数的形式传入 Repository 中。最后,同样还是利用 LiveData,将 User 数据传递到上一层,即 View 层。 + + ```java + public class UserViewModel extends AndroidViewModel{ + private LiveData user; + + private UserRepository userRepository; + + private String userName = "MichaelYe"; + + public UserViewModel(Application application){ + super(application); + UserDatabase database = MyApplication.getUserDatabase(); + UserDao userDao = database.userDao(); + userRepository = new UserRepository(userDao, MyApplication.getApi()); + user = userRepository.getUser(userName); + } + + public LiveData getUser(){ + return user; + } + + public void refresh(){ + userRepository.refresh(userName); + } + } + ``` + + g. View 层 + + 在 Activity 中,使用了 DataBinding 组件和下拉刷新组件。当 User 数据发生变化时,自动通过回调方法得到通知,在接收到通知后,将数据交给布局文件进行处理。 + + ```java + public class MainActivity extends AppCompatActivity{ + @Override + protected void onCreate(Bundle savedInstanceState){ + super.onCreate(savedInstance); + final ActivityMainBinding activityMainBinding = + DataBindingUtil.setContentView(this,R.layout.activity_main); + final UserViewModel userViewModel = + new ViewModelProvider(this).get(UserViewModel.class); + userViewModel.getUser().observe(this, new Observer(){ + @Override + public void onChanged(User user){ + if(user != null){ + activityMainBinding.setUser(user); + } + } + }); + + final SwipeRefreshLayout swipeRefresh = + activityMainBinding.swipeRefresh; + swipeRefresh.setOnRefreshListener( + new SwipeRefreshLayout.onRefreshListener(){ + @Override + public void onRefresh(){ + userViewModel.refresh(); + swipeRefresh.setRefreshing(false); + } + }); + } + } + ``` + + 在布局文件中处理 Activity 传递来的数据对象。其中用到了 CircleImageView 组件,用于生成圆形头像。 + + ```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ``` + + 在布局文件中,还通过自定义 BindingAdapter,实现图片的加载。具体的加载工作,交给 Picasso 完成。 + + ```java + public class ImageViewBinding{ + @BindingAdapter(value={"image","defaultImageResource"},requireAll = false) + public static void setImage(ImageView imageView, String imageUrl, int imageResource){ + if(!TextUtils.isEmpty(imageUrl)){ + Picasso.get() + .load(imageUrl) + .placeholder(R.drawable.ic_default) + .error(R.drawable.ic_error) + .into(imageView); + } else { + imageView.setImageResource(imageResource); + } + } + } + ``` + +### 10.5. 总结 + +数据驱动视图,主要利用了 LiveData 来实现层层通知。很重要的一点是,需要保证数据的单一来源,对于要展示的数据,只通过数据库获取。从网络获取的数据不直接进行展示,而是将其写入数据库后,再由数据库通知外界进行更新。另外,层与层之间严禁跨层沟通,下一层只对其直接的上一层负责。这样虽然项目写起来会比较耗费时间,在小型项目中会显得臃肿。但是一旦代码量大量增加时,这样的做法能够让你避免陷入技术债务,也方便对每一层进行单元测试,因为层与层之间的关系是相对独立的。 + +Jetpack 的各组件可以结合使用,也可以单独使用。架构符合 mvvm 规范的应用程序,并非一定要用到所有的 Jetpack 组件,但 Google 在 Jetppack 各组件的研发过程中,确实考虑了结合使用的情形。例如,Room 组件可以单独使用,但它原生就支持 LiveData,因此,结合 LiveData 使用效果会更强大。 + +值得注意的是,关于架构 MVVM 应用程序,Google 官方还推荐使用一个名为 Dagger 的依赖注入库。Dagger 库原是 Square 公司的项目,现由 Google 维护。Dagger 库的作用是解耦对象之间的依赖关系。 + +架构应用程序不可能有十全十美的解决方案,Google 推出 Jetpack 是希望能解决工程师的困扰,为大家指明一个大致的方向,但它不一定就是一个完美的方案。由于产品形态各异,项目的架构不可能有统一的解决方案。学习 Jetpack 是必要的,它让我们了解了 Google 官方有哪些解决方案,但它不是"万能钥匙"。在世纪开发中需要考虑的因素太多,建议可以在自己的项目中引入部分 Jetpack 组件进行尝试,以判断是否符合当前项目的需求。 diff --git "a/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/\347\254\2541\347\253\240-\345\210\235\350\257\206Jetpack.md" "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/\347\254\2541\347\253\240-\345\210\235\350\257\206Jetpack.md" new file mode 100644 index 00000000..2ac44595 --- /dev/null +++ "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/\347\254\2541\347\253\240-\345\210\235\350\257\206Jetpack.md" @@ -0,0 +1,58 @@ +# 第 1 章 初识 Jetpack + +### 1.1. Android 应用程序架构设计标准的缺失概论 + +Android 的应用架构始终处于一个混乱的阶段,Android 工程师很困惑,他们不确定自己使用的架构是否真的是最佳方案。这不仅增加了工程师的学习成本,还可能最终导致他们开发出的应用程序质量参差不齐。 + +Android 工程师希望 Google 官方可以推出并维护一些关于架构的组件或指南,这样他们就可以将更多的精力放在自己的业务代码上了。Google 也意识到了这个问题,这便有了 Jetpack,Jetpack 正是为了解决这些问题而诞生的。 + +### 1.2. 什么是 Jetpack + + Jetpack 是一套库、工具和指南,可以帮助开发者更轻松地编写应用程序。Jetpack 中的组件可以帮助开发者遵循最佳做法、摆脱编写样板代码的工作并简化复杂的任务,以便他们能将精力集中放在业务所需的代码上。 + +Jetpack 主要包括 4 个方面,分别是架构(Architecture)、界面(UI)、行为(Behavior)和基础(Foundation)。其中,架构是关注的重点。 + +### 1.3. Jetpack 与 AndroidX + +在 2018 年 Google 宣布用 AndroidX 代替 Android Support Library,Android Support Library 在版本 28 之后就不再更新了,未来的更新会在 AndroidX 中进行。不仅如此,AAC(Android Architecture Component)中的组件也被并入 AndroidX。 + +为什么 Jetpack 组件需要以兼容包的形式存在,而不是成为 Framework 的一部分呢?为了提供向后兼容,使 Jetpack 组件能够应对更加频繁的更新。除了 Android Support Library 和 AAC,其他一些需要频繁更新和迭代的特定也被并入了 AndroidX,例如 Emoji。 + +### 1.4. 迁移至 AndroidX + +如果从未在项目中使用过 Jetpack 组件,现在希望将项目迁移至 AndroidX,那么可以在菜单栏中选择 “Refactor” -> "Migrate to AndroidX..." 选项,将项目迁移至 AndroidX。 + +迁移之后,打开项目中的 gradle.properties 文件,可以看到下面这两行代码: + +``` +android.useAndroidX = true +android.enableJetifier = true +``` + +上述代码的含义如下: + +* android.useAndroidX 表示是否使用 AndroidX。 +* android.enableJetifier 表示是否将第三方库迁移至 AndroidX。 + +### 1.5. 新建项目默认支持 AndroidX + +如果 Android Studio 为最新版本,那么在新建一个项目时,应该能在创建过程中看到 “Use androidx.* artifacts” 这个选项。这表示,新创建的项目会默认配置对 AndroidX 的支持。 + +如果没有看到此选项,那么检查 SDK 配置。通过 “Tools” -> "SDK Manager" 打开配置界面,确保已经安装了 Android 9.0 及以上版本的 SDK。 + +### 1.6. 总结 + + + + + + + + + + + + + + + diff --git "a/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/\347\254\2542\347\253\240-LifeCycle.md" "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/\347\254\2542\347\253\240-LifeCycle.md" new file mode 100644 index 00000000..a4c606cf --- /dev/null +++ "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/\347\254\2542\347\253\240-LifeCycle.md" @@ -0,0 +1,344 @@ +# 第 2 章 LifeCycle + +### 2.1. LifeCycle 的诞生 + +LifeCycle 可以帮助开发者创建可感知生命周期的组件。这样,组件便能够在其内部管理自己的生命周期,从而降低模块间的耦合度,并降低内存泄漏发生的可能性。LifeCycle 不只对 Activity/Fragment 有用,在 Service 和 Application 中也能大显身手。 + +### 2.2. 使用 LifeCycle 解耦页面与组件 + +#### 2.2.1. 案例分析 + +假设有这样一个常见的需求:在用户打开某个页面时,获取用户当前的地理位置。面对该需求,通常会这样写代码。 + +```kotlin +public class MainActivity extends AppCompatActivity +{ + @Override + protected void onCreate(Bundle savedInstanceState){ + // 初始化位置管理器 + iniLocationManager(); + } + + @Override + protected void onResume(){ + // 开始获取用户的地理位置 + setGetLocation(); + } + + @Override + protected void onPause(){ + // 停止获取用户的地理位置 + stopGetLocation(); + } +} +``` + +从以上代码可以看出,获取地理位置这个需求的实现,与页面的生命周期息息相关。如果希望将获取地理位置这一功能独立成一个组件,那么生命周期是必须要考虑在内的。不得不在页面生命周期的各个回调方法中,对组件进行通知,因为组件不能主动感知生命周期的变化。 + +#### 2.2.2. LifeCycle 的原理 + +Jetpack 提供了两个类:LifecycleOwner(被观察者)和 LifecycleObserver(观察者)。即通过观察着模式,实现对页面生命周期的监听。 + +通过查看 SupportActivity 的源码,可以看到,在新版本的 SDK 包中,Activity 已经默认实现了 LifecycleOwner 接口。LifecycleOwner 接口中只有一个 getLifecycle(LifecycleObserver observer) 方法,LifecycleOwner 正是通过该方法实现观察者模式的。源码示例如下: + +```kotlin +public class SupportActivity extends Activity implements LifecycleOwner, Component { + ... + private LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this); + + public SupportActivity(){ + + } + + public Lifecycle getLifecycle() { + return this.mLifecycleRegistry; + } + + ... +} +``` + +从以上源码可知,SupportActivity 已经实现了被观察者应该实现的那一部分代码。因此,不需要再去实现这部分代码。当希望监听 Activity 的生命周期时,只需要实现观察者那一部分的代码,即让自定义组件实现 LifecycleObserver 接口即可。该接口没有接口方法,无须任何具体实现。 + +#### 2.2.3. 解决方案 + +现在利用 LifeCycle 改写该需求。目的是将该功能从 Activity 中独立出去,在减少耦合度的同时,又不影响对生命周期的监听。 + +1. 编写一个名为 MyLocationListener 的类。该类就是自定义组件,需要让该组件实现 LifecycleObserver 接口。与获取地理位置相关的代码在该类中完成。 + + 对于组件中那些需要在页面生命周期发生变化时得到通知的方法,需要在这些方法中使用 @OnLifecycleEvent(Lifecycle.Event.ON_XXX) 标签进行标识。这样,当页面生命周期发生变化时,这些被标识过的方法便会被自动调用。如下所示: + + ```kotlin + public class MyLocationListener implements LifecycleObserver{ + public MyLocationListener(Activity context, OnLocationChangedListener onLocationChangedListener){ + // 初始化操作 + iniLocationManager(); + } + + /** + * 当 Activity 执行 onResume() 方法时,该方法会被自动调用 + */ + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) + private void startGetLocation(){ + Log.d(TAG,"startGetLocation()"); + } + + /** + * 当 Activity 执行 onPause() 方法时,该方法会被自动调用 + */ + @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) + private void stopGetLocation(){ + Log.d(TAG,"stopGetLocation()"); + } + + /** + * 当地理位置发生变化时,通过该接口通知调用者 + */ + public interface OnLocationChangedListener{ + void onChanged(double latitude, double longitude); + } + // 其他一些业务代码 + } + ``` + +2. 在 MainActivity 中,只需要引用 MyLocationListener 即可,不用再关心 Activity 生命周期变化对该组件所带来的影响。生命周期的管理完全交给 MyLocationListener 内部自行处理。在 Activity 中要做的只是通过 getLifecycle().addObserver() 方法,将观察者与被观察者绑定起来,代码如下所示: + + ```kotlin + public class MainActivity extends AppCompatActivity + { + private MyLocationListener myLocationListener; + + @Override + protected void onCreate(Bundle savedinstanceState) + { + myLocationListener = new MyLocationListener(this, + new MyLocationListener.OnLocationChangeListener(){ + @Override + public void onChanged(double latitude, double longitude){ + // 展示收到的位置信息 + } + }); + + //将观察者与被观察者绑定 + getLifecycle().addObserver(myLocationListener); + } + + } + ``` + + LifeCycle 完美解决了组件对页面生命周期的依赖问题,使组件能够自己管理其生命周期,而无须在页面中对其进行管理。这无疑大大降低了代码的耦合度,提供了组件的复用程度,也杜绝了由于对页面生命周期管理的疏忽而引发的内存泄漏问题,这在项目工程量大的情况下是非常有帮助的。 + + 除 Activity 之外,在新版本的 SDK 中,Fragment 同样也默认实现了 LifecycleOwner 接口。因此,以上案例同样适用于 Fragment。Fragment 的源码如下所示。 + + ```kotlin + public class Fragment implements ComponentCallbacks, + OnCreateContextMenuListener, + LifecycleOwner, + ViewModelStoreOwner + { + ... + LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this); + + public Lifecycle getLifecycle(){ + return this.mLifecycleRegistry; + } + + ... + } + ``` + +### 2.3. 使用 LifecycleService 解耦 Service 与组件 + +#### 2.3.1. LifeCycleService 基本介绍 + +拥有生命周期概念的组件除了 Activity 和 Fragment,还有一个非常重要的组件是 Service。为了便于对 Service 生命周期的监听,达到解耦 Service 与组件的目的,Android 提供了一个名为 LifecycleService 的类。该类继承自 Service,并实现了 LifecycleOwner 接口。与 ActivityFragment 类似,它也提供了一个名为 getLifecycle() 的方法以供使用。LifecycleService 的源码如下所示: + +```kotlin +public class LifecycleService extends Service implements LifecycleOwner{ + ... + private final ServiceLifecycleDispatcher mDispatcher = new ServiceLifecycleDispatcher(this); + + @Override + public Lifecycle getLifecycle() { + return mDispatcher.getLifecycle(); + } + ... +} +``` + +#### 2.3.2. LifecycleService 的具体使用方法 + +1. 首先,需要在 app 的 build.gradle 文件中添加相关依赖。 + + ```groovy + dependencies { + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + } + ``` + +2. 添加依赖后,便可以使用 LifecycleService 类了。创建一个名为 MyService 的类,并让它继承自 LifecycleService。由于 LifecycleService 是 Service 的直接子类,所以使用与普通 Service 没有差别。 + + ```kotlin + public class MyService extends LifecycleService{ + private MyServiceObserver myServiceObserver; + + public MyService() + { + myServiceObserver = new MyServiceObserver(); + // 将观察者与被观察者绑定 + getLifecycle().addObserver(myServiceObserver); + } + } + ``` + +3. 接下来是 MyServiceObserver 类,该类需要实现 LifecycleObserver 接口。与此同时,使用 @OnLifecycleEvent 标签对那些希望能够在 Service 生命周期发生变化时得到同步调用的方法进行标识。 + + ```kotlin + public class MyServiceObserver implements LifecycleObserver{ + private String TAG = this.getClass().getName(); + + // 当 Service 的 onCreate() 方法被调用时,该方法会被调用 + @OnLifecycleEvent(Lifecycle.Event.ON_CREATE) + private void startGetLocation(){ + Log.d(TAG,"startGetLocation()") + } + + // 当 Service 的 onDestory() 方法被调用时,该方法会被调用 + @OnLifecycleEvent(Lifecycle.Event.ON_DESTORY) + private void stopGetLocation(){ + Log.d(TAG,"stopGetLocation()") + } + } + ``` + +4. 最后,在页面中利用两个 Button,控制 Service 的启动和停止,测试代码。 + + ```kotlin + findViewById(R.id.btnStartService).setOnClickListener( + new View.OnClickListener(){ + @Override + public void onClick(View v){ + // 启动服务 + Intent intent = new Intent(MainActivity.this,MyService.class); + startService(intent); + } + } + ); + + findViewById(R.id.btnStopService).setOnClickListener( + new View.OnClickListener(){ + @Override + public void onClick(View v){ + // 停止服务 + Intent intent = new Intent(MainActivity.this,MyService.class); + stopService(intent); + } + } + ); + ``` + +5. 通过 LogCat 中的日志可以看到,随着 Service 生命周期的变化,MyServiceObserver 中带有 @OnLifecycleEvent 标签的方法被自动调用了。这样,便实现了组件对 Service 生命周期的监听。 + + ``` + com.xxx.xxx.xxx.MyServiceObserver: startGetLocation() + com.xxx.xxx.xxx.MyServiceObserver: stopGetLocation() + ``` + +通过以上实例可以看出,当 Service 的生命周期发生变化时,不再需要主动对组件进行通知,组件能在其内部自行管理好生命周期所带来的变化。LifecycleService 很好地实现了组件与 Service 之间的解耦。 + +### 2.4. 使用 ProcessLifecycleOwner 监听应用程序的生命周期 + +#### 2.4.1. ProcessLifecycleOwner 存在的意义 + +具有生命周期的系统组件除 Activity、Fragment、Service 外,还有 Application。很多时候,会遇到这样的需求:想知道应用程序当前处在前台还是后台,或者当应用程序从后台回到前台时,能够得到通知。有不少方案能够实现该需求,但都不够好。在此之前,Google 并没有为该需求提供官方解决方案,直到 LifeCycle 的出现。LifeCycle 提供了一个名为 ProcessLifecycleOwner 的类,以方便知道整个应用程序的生命周期情况。 + +#### 2.4.2. ProcessLifecycleOwner 的具体使用方法 + +1. 首先,需要在 app 的 build.gradle 文件中添加相关依赖。 + + ```groovy + dependencies{ + implementation "androidx.lifecycle:lifecycle-extensions: 2.2.0" + } + ``` + +2. ProcessLifecycleOwner 的使用方法与 Activity、Fragment 和 Service 是类似的,其本质也是观察者模式。由于要观察的是整个应用程序,因此,需要在 Ap plication 中进行相关代码的编写。 + + ```java + public class MyApplication extends Application{ + @Override + public void onCreate(){ + super.onCreate(); + ProcessLifecycleOwner.get().getLifecycle() + .addObserver(new ApplicationObserver()); + } + } + ``` + +3. 定义一个名为 ApplicationObserver 的类,让该类实现 LifecycleObserer 接口,以负责对应用程序生命周期的监听。 + + ```java + public class ApplicationObserver implements LifecycleObserer + { + private String TAG = this.getClass().getName(); + + /** + * 在应用程序的整个生命周期中只会被调用一次 + */ + @OnLifecycleEvent(Lifecycle.Event.ON_CREATE) + public void onCreate(){ + Log.d(TAG, "Lifecycle.Event.ON_CREATE"); + } + + /** + * 当应用程序在前台出现时被调用 + */ + @OnLifecycleEvent(Lifecycle.Event.ON_START) + public void onStart(){ + Log.d(TAG, "Lifecycle.Event.ON_START"); + } + + /** + * 当应用程序在前台出现时被调用 + */ + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) + public void onResume(){ + Log.d(TAG, "Lifecycle.Event.ON_RESUME"); + } + + /** + * 当应用程序退出到后台时被调用 + */ + @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) + public void onPause(){ + Log.d(TAG, "Lifecycle.Event.ON_PAUSE"); + } + + /** + * 当应用程序退出到后台时被调用 + */ + @OnLifecycleEvent(Lifecycle.Event.ON_STOP) + public void onStop(){ + Log.d(TAG, "Lifecycle.Event.ON_STOP"); + } + + /** + * 永远不会被调用,系统不会分发调用 ON_DESTORY 事件 + */ + @OnLifecycleEvent(Lifecycle.Event.ON_DESTORY) + public void oDestory(){ + Log.d(TAG, "Lifecycle.Event.ON_DESTORY"); + } + } + ``` + +通过以上实例可以看出,有了 ProcessLifecycleOwner,可以轻而易举地获知应用程序何时退到后台,何时进入前台,进而执行一些业务操作。它使用起来非常简单,并且不会给项目增加任何的耦合度,但有一下几点需要注意。 + +* ProcessLifecycleOwner 是正对整个应用程序的监听,与 Activity 数量无关,有一个 Activity 和多个 Activity,对 ProcessLifecycleOwner 来说是没有区别的。 +* Lifecycle.Event.ON_CREATE 只会被调用一次,而 Lifecycle.Event.ON_DESTORY 永远不会被调用。 +* 当应用程序从后台回到前台,或者应用程序被首次打开时,会一次调用 Lifecycle.Event.ON_START 和 Lifecycle.Event.ON_RESUME。 +* 当应用程序从前台退到后台(用户按下 Home 键或任务菜单键),会依次调用 Lifecycle.Event.ON_PAUSE 和 Lifecycle.Event.ON_STOP。需要注意的是,这两个方法的调用会有一定的延后。这是因为系统需要为 “屏幕旋转,由于配置发生变化而导致 Activity 重新创建” 的情况预留一些时间。也就是说,系统需要保证当设备出现这种情况时,这两个事件不会被调用。因为当旋转屏幕时,应用程序并没有退出后台,它只是进入了横/竖屏模式而已。 + +### 2.5. 总结 + +所有具有生命周期的组件都能够使用 LifeCycle。这包括 Activity、Fragment、Service 和 Application。LifeCycle 组件存在的主要意义是帮助解耦,让自定义组件也能够感受到生命周期的变化。在没有 LifeCycle 之前,每次当系统组件的生命周期发生变化时,都需要留意这会对自定义组件造成哪些影响。有了 LifeCycle 之后,在自定义组件内部便可以管理好其生命周期,不再需要担心组件的内存泄漏等问题了,组件使用起来也更加方便和安全。 diff --git "a/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/\347\254\2543\347\253\240-Navigation.md" "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/\347\254\2543\347\253\240-Navigation.md" new file mode 100644 index 00000000..75522ec5 --- /dev/null +++ "b/BookNote/Android Jectpack \345\272\224\347\224\250\346\214\207\345\215\227/\347\254\2543\347\253\240-Navigation.md" @@ -0,0 +1,649 @@ +# 第 3 章 Navigation + +### 3.1. Navigation 的诞生 + +单个 Activity 嵌套多个 Fragment 的 UI 架构模式,已经被大多数 Android 工程师 所接受和采用。但是,对 Fragment 的管理一直是一件比较麻烦的事情 。工程师 需要通过 FragmentManager 和 FragmentTransaction 来管理 Fragment 之间的切换。页面的切换通常还包括对应用程序 App bar 的管理、Fragment 间的切换动画,以及 Fragment 间的参数传递。纯代码的方式使用起来不是特别友好,并且Fragment 和 App bar 在管理和使用的过程中显得很混乱。 + +为此,Jetpack 提供了一个名为 Navigation 的组件,旨在方便管理页面和 App bar。它具有以 下优势。 + +* 可视化的页面导航图 ,类似于 Apple Xcode 中 的 StoryBoard ,便于我们理清页面间的关系。 +* 通过 destination 和 action 完成页面间的导航。 +* 方便添加页面切换动画。 +* 页面间类型安全的参数传递。 +* 通 过 Navigation UI 类 , 对菜单、底部导航、抽屉莱单导航进行统一的管理。 +* 支持深层链接 DeepLink 。 + +### 3.2. Navigation 的主要元素 + +Navigation 中的主要元素: + +1. Navigation Graph。这是一种新型的 XML 资源文件,其中包含应用程序所有的页面,以及页面间的关系。 + +2. NavHost Fragment 。这是一个特殊的 Fragment ,可以认为它是其他Fragment 的 “容器”,Navigation Graph 中的 Fragment 正是通过NavHostFragment 进行展示的。 +3. NavController 。这是一个 Java/Kotlin 对象,用于在代码中完成 Navigation Graph 中具体的页面切换工作。 + +当你想切换 Fragment 时,使用 NavController 对象,告诉它你想要去 Navigation Graph 中的哪个 Fragment ,NavController 会将你想去的 Fragment 展示在 NavHostFragment 中。 + +### 3.3. 使用 Navigation + +#### 3.3.1. 创建 NavigationGraph + +新建一个 Android 项目后,依次选中 res 文件夹 -> New -> Android Resource File, 新建一个 Navigation Graph 文件。将 Filename 设置为 “nav_graph",Resource type 设置为 “Navigation”。 + +单击 “OK” 按钮,NavigationGraph 就创建完成了。 + +所生成的 nav_graph.xml 文件与普通布局文件类似,也有 Design 和 Text 面板。 在此,能在 Destinations 面板中,看见 “No NavHostFragments found” 的提示。 + +需要注意的是,Navigation 的使用需要依赖于相关支持库,因此 Android Studio +可能会询问你,是否自动添加相关依赖,单击 OK 即可。 + +当然,也可以在 build.gradle 文件中手动添加依赖,示例如下。 + +```groovy +dependencies { + def nav_version = "2.3.0-alpha05" + implementation "android.navigation:navigation-fragment: $nav" + implementation "androidx.navigation:navigation-ui:Snav" +} +``` + +#### 3.3.2. 添加 NavHostFragment + +NavHostFragment 是 一个特殊的 Fragment,需要将其添加到 Activity 的布 +局文件中,作为其他 Fragment 的容器。 + +```xml + + + + + +``` + +请注意,下面这一行代码在告诉系统,这是 一个特殊的 Fragment。 + +```xml +android:name = "android.navigation.fragment.NavHostFragment" +``` + +将 app:defaultNavHost 属性设置为 true,则该 Fragment 会自动处理系统返回键。 即当用户按下手机的返回按钮时,系统能自动将当前所展示的 Fragment 退出。 + +```xml +app: defaultNavHost="true" +``` + +app:navGraph 属性用于设置该 Fragment 对应的导航图。 + +```xml +app: navGraph="@navigation/nav_graph" +``` + +添加 NavHostPragment 之后,再回到导航图上。此时,在 Destinations 面板中 可以看见刚才设置的 NavHostFragment 。 + +#### 3.3.3. 创建 destination + +依次单击加号按钮、“Crcatenewdestination” 按钮,创建—个destination。 + +destination 是“目的地” 的意思,代表着想去的页面。它可以是 Fragment 或Activity,但最常见的是 Fragment,因为 Navigation 组件的作用是方便开发者在 一个 Activity 中管理多个 Fragment。在此,通过 destination 创建 一个名为 MainFragment 的 Fragment 。 + +接着,再次单击加号按钮,此时出现了刚才创建的 MainFragment 对应的布局文件 fragment_main。单击该文件。 + +面板中出现了一个 mainFragment 。“ Start” 表示该 MainFragment 是起始Fragment ,即 NavHostFragment 容器首先展示的 Fragment。 + +查看布局文件的内容: + +```xml + + + + + +``` + +可以看到,在 navigation 标签下有 一个 startDestination 属性,该属性指定起始 destination 为 mainFragment。 + +```xml +app:startDestination="@id/mainFragment" +``` + +运行程序,可以看到一个空白的 Fragment,即 destination 所指定的 mainFragment。 + +#### 3.3.4. 完成 Fragment 页面切换 + +接下来看看 Navigation 如何实现 Fragment 页面的切换。 + +与创建 MainFragment 的方式类似,先创建一个 SecondFragment。 + +单击 mainFragment,用鼠标选中其右侧的圆圈,并拖拽至右边的 secondFragment,松开鼠标。 + +![](img/navigation拖拽.png) + +此时出现一个从 mainFragment 指向 secondFragment 的箭头。 + +切换到 Text 面板,查看布局文件。可以看到多了一个 \ 标签,app:destination 属性表示它的目的地是 secondFragment。 + +```xml + + + + + + + + + + + +``` + +#### 3.3.5. 使用 NavCOntroller 完成导航 + +还需要通过 NavController 对象,在代码中完成具体的页面跳转工作。 + +在 MainFragment 的布局文件中添加一个Button。 + +```xml + + + + + +