<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          用Jetpack Compose寫一個玩安卓App!

          共 27783字,需瀏覽 56分鐘

           ·

          2021-07-30 19:23

          微信改了推動機制,真愛請星標(biāo)本公號
          公眾號回復(fù)加入BATcoder技術(shù)群BAT

          作者:Zhujiang

          https://juejin.cn/user/3913917127985240/posts

          前言


          本文由一場比賽引起,先看看比賽要求。


          是不是很簡單,只需要兩個頁面。


          嗯。。。。差不多就是這樣我就直接提交了,不過得不得獎無所謂,開心就好。

          Compose 和 Flutter

          在寫完這個小Demo之后,有一個感覺,Compose和Flutter他們兩個。。。。不能說有點相似,只能說完全一樣!

          Compose完全打破了我之前對安卓的看法。。和Flutter簡直是一個模子里刻出來的,連命名都有些相似,雖然Google官方說不會放棄Java,但是看看協(xié)程和 Compose,真的想說一句:我信你個鬼,你個糟老頭子壞的很!

          雖然二者很像,但是發(fā)力的方向是完全不同的:

          Flutter 大家都知道,是個 跨平臺 的 UI 框架,注意,只是 UI 框架!各種復(fù)雜實現(xiàn)全都是安卓和蘋果原生寫的,可以一套代碼多處使用,特別是現(xiàn)在 Flutter 2.0 的發(fā)布,更是不得了,支持安卓、蘋果、windows、mac、網(wǎng)頁,不得不說實在是太強了。

          那 Compose 為什么會出現(xiàn)?又或者說它有什么用呢?

          Compose 為什么會出現(xiàn)


          這一小節(jié)的內(nèi)容很明確,寫 Compose 為什么會出現(xiàn)。這個問題其實寫安卓的都比較清楚,有的就算不清楚也可以猜出二三。

          以前咱們編寫安卓程序的時候頁面都寫在 res -> layout 文件夾下,以 xml 的形式展現(xiàn),這樣的好處顯而易見,將邏輯代碼和頁面徹底分開,確實分開了,但是使用的時候又需要去 findViewById,xml 布局加載的時候又需要耗費大量時間,加載完成之后還需要通過反射來獲取 View,又是一大耗時操作。。

          記得之前有個大神寫過一篇文章,里面通過直接 new 布局和 xml 方式寫布局進(jìn)行比較,速度甚至相差幾十倍,然后各種各樣的優(yōu)化就出來了,好像有個團(tuán)隊甚至自己編寫了一整套布局,完全沒有使用 xml,也是神人了!

          寫到這里應(yīng)該都知道 Compose 為什么會出現(xiàn)了吧!

          Compose 有什么


          Compose 還有一個重要的特點——聲明式。

          聲明式?這是什么東西?怎么說呢,就是不以命令方式改變前端視圖的情況下呈現(xiàn)應(yīng)用界面,從而使編寫和維護(hù)應(yīng)用界面變得更加容易。

          不解釋還好,一解釋更加懵逼。。。簡單來說就是通過對數(shù)據(jù)的改變而改變布局,不用以前 findViewById 那樣遍歷樹,減少出錯的可能性,而且軟件維護(hù)復(fù)雜性會隨著需要更新的視圖數(shù)量而增長。不行不行,我自己都有點懵了,給大家舉個例子吧!

          其實咱們所熟知的 MVVM 其實就是數(shù)據(jù)驅(qū)動布局改變的,為什么這樣說呢?你想一下,你的 ViewModel 中是不是通常會定義一個 LiveData,然后在你的 Activity 或者 Fragment 中進(jìn)行 observe,然后將你的 UI 操作放到這里,當(dāng)數(shù)據(jù)改變的時候相應(yīng)地去修改你的 UI,這樣說的話是不是好理解一些呢?

          準(zhǔn)備工作


          我個人的一個小習(xí)慣,學(xué)習(xí)什么新東西的時候就會寫個 Demo ,之前我寫過一個 MVVM 版的玩安卓,而且還為這個項目寫過一個系列的文章,感興趣的可以去我的文章列表看看。

          這次寫完官方比賽的小 Demo 之后覺得 Compose 挺好玩,并且好多大佬都說 Compose 是未來的趨勢,于是就想著把那個 MVVM 版的玩安卓改用 Compose 實現(xiàn)一下試試。

          先來看看成品



          看著是不是也還可以?那就開始著手編寫吧!

          準(zhǔn)備工作

          由于之前已經(jīng)編寫過 MVVM 版本的玩安卓了,所以說很多東西咱們就可以直接進(jìn)行使用了,比如說一下圖片資源,又比如說數(shù)據(jù)、網(wǎng)絡(luò)請求等等都是現(xiàn)成了,咱們要做的只是將以前的 xml 布局改成 Compose 即可。

          聽著是不是很簡單?但是寫的時候有點懵,這還是我之前寫過 Flutter 的情況下,如果大家沒有寫過 Flutter 或者 SwiftUI 的話看起來可能會更懵,因為里面好多東西都顛覆了我對安卓的看法。。。

          為了區(qū)分和之前 MVVM 版本的區(qū)別,我把這次的 Compose 的版本分支改為了 main 分支,大家下載代碼的時候切換下分支就可以了,或者直接下載 main 分支的代碼也可以。

          引入依賴


              
          // `Compose`
          implementation "androidx.compose.ui:ui:$compose_version"
          implementation "androidx.compose.runtime:runtime-livedata:$compose_version"
          implementation "androidx.compose.material:material:$compose_version"
          implementation "androidx.compose.ui:ui-tooling:$compose_version"
          implementation "androidx.activity:activity-compose:1.3.0-alpha03"
          implementation "androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha02"
          implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$compose_version"
          implementation "androidx.compose.foundation:foundation:$compose_version"
          implementation "androidx.compose.foundation:foundation-layout:$compose_version"
          implementation "androidx.compose.material:material-icons-extended:$compose_version"

          androidTestImplementation "androidx.compose.ui:ui-test:$compose_version"
          androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"

          // navigation
          implementation "androidx.navigation:navigation-compose:1.0.0-alpha08"

          what?不是一個 Compose 庫嗎?干嘛引入這么多?我之前也是這么想的,但是在用的時候一個個又加進(jìn)去的。。。如果不知道每一個包是什么意思的話可以去官方文檔中查看下,不過光看依賴名稱基本就知道是什么意思了。。。

          如果你也想像我一樣在以前的項目中使用 Compose,那么下面的這一步千萬別忘了,我就是忘了添加下面這一步找了整整一天的錯

              
          android {
              …………
              buildFeatures {
                  `Compose` true
                  viewBinding true
              }

              `Compose`Options {
                  kotlinCompilerExtensionVersion compose_version
                  kotlinCompilerVersion kotlin_version
              }
          }

          就是上面的,一定別忘了進(jìn)行配置,不然找錯誤能找死。。。給我提示的是 Kotlin 內(nèi)部 JVM 錯誤,搞得我我都準(zhǔn)備給 Kotlin 提 Bug 了。。。

          大家可能看到上面的依賴中有 navigation,看名字就知道是專門為 Compose 寫的,這也是 Compose 跳轉(zhuǎn)的重要工具,也許有更好的,只是我沒有發(fā)現(xiàn)吧。

          上面也不止一次提到 Compose 顛覆了我之前對安卓的看法,之前的我認(rèn)為安卓就是一堆 Activity 加上 Fragment ,但是寫了 Compose 之后我發(fā)現(xiàn)并不是這樣的,好多官方的 Demo 只有一個 Activity。

          看得我有點懵,但是后來想了想就明白了,還是類似 Flutter ,在 Flutter 中不也是一個 Activity 嘛,每一個頁面也都是一個 Widget!跳轉(zhuǎn)也不是之前的 Intent ,而是路由,。現(xiàn)在的 Compose 也是一樣,只不過 Widget 改為了 Composable,路由改為了 navigation。

          今天就寫一下首頁框架吧,就是一個底部導(dǎo)航欄加上四個頁面,實現(xiàn)點擊進(jìn)行切換。

          新建 Activity


          先來新建一個 Activity 吧,這個項目之后就用這一個 Activity,看下 AndroidManifest:

              
          <activity
              android:name=".`Compose`.NewMainActivity"
              android:theme="@style/AppTheme.NoActionBars">

              <intent-filter>
                  <action android:name="android.intent.action.MAIN" />

                  <category android:name="android.intent.category.LAUNCHER" />
              </intent-filter>
          </activity>

          這沒什么說的,就是把之前的首頁改為了新的首頁,其它的 Activity 都已經(jīng)用不到了。

              
          class NewMainActivity : AppCompatActivity() {

              override fun onCreate(savedInstanceState: Bundle?) {
                  super.onCreate(savedInstanceState)
                  setContent {
                      PlayTheme {
                          Home()
                      }
                  }
              }

          }

          這個 Activity 很簡單,大家發(fā)現(xiàn)了沒有,咱們熟悉的 setContentView 方法沒有調(diào)用,取而代之的是 setContent 方法,不過不重要,這是 Compose 的固定寫法而已,當(dāng)然也可以將 Compose 寫到 xml 中然后通過 findViewById 來找到再進(jìn)行操作,還可以直接 new 出一個 Compose 來進(jìn)行操作,這里咱們就選擇 setContent 這種方法。

          看下  setContent  這個方法吧:

              
          public fun ComponentActivity.setContent(
              parent: CompositionContext? = null,
              content: @Composable ()
           -> Unit
          ) {
              val existingComposeView = window.decorView
                  .findViewById<ViewGroup>(android.R.id.content)
                  .getChildAt(0as? ComposeView

              if (existingComposeView != null) with(existingComposeView) {
                  setParentCompositionContext(parent)
                  setContent(content)
              } else ComposeView(this).apply {
                  // Set content and parent **before** setContentView
                  // to have ComposeView create the composition on attach
                  setParentCompositionContext(parent)
                  setContent(content)
                  setContentView(this, DefaultActivityContentLayoutParams)
              }
          }

          明白了吧?這是 ComponentActivity 的一個擴展方法,里面其實還會執(zhí)行 setContentView 方法的,并沒有什么神器的魔法。。

          創(chuàng)建 Compose

          雖然這里選擇了 setContent 這種方法,但是還是說下別的情況下怎么使用吧。

          可以將 ComposeView 放在 XML 布局中,就像放置其他任何 View 一樣:

              
          <?xml version="1.0" encoding="utf-8"?>
          <LinearLayout
              xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent">


              <TextView
                  android:id="@+id/hello_world"
                  android:layout_width="match_parent"
                  android:layout_height="wrap_content"
                  android:text="Hello Android!" />


              <androidx.compose.ui.platform.ComposeView
                  android:id="@+id/compose_view"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent" />


          </LinearLayout>

          上面的布局很簡單,大家注意看,咱們把 Compose 直接寫在了 xml 中,這樣也是可以進(jìn)行使用的,怎么使用呢?

              
          class ExampleFragment : Fragment() {

              override fun onCreateView(
                  inflater: LayoutInflater,
                  container: ViewGroup?,
                  savedInstanceState: Bundle?
              )
          : View {
                  // Inflate the layout for this fragment
                  return inflater.inflate(
                      R.layout.fragment_example, container, false
                  ).apply {
                      findViewById<ComposeView>(R.id.compose_view).setContent {
                          // In Compose world
                          MaterialTheme {
                              Text("Hello Compose!")
                          }
                      }
                  }
              }
          }

          是不是很簡單,當(dāng)然也可以直接 new 出一個 Compose 來進(jìn)行操作:

              
          class ExampleFragment : Fragment() {

              override fun onCreateView(
                  inflater: LayoutInflater,
                  container: ViewGroup?,
                  savedInstanceState: Bundle?
              )
          : View {
                  return ComposeView(requireContext()).apply {
                      setContent {
                          MaterialTheme {
                              // In Compose world
                              Text("Hello Compose!")
                          }
                      }
                  }
              }
          }

          好的,這樣其實已經(jīng)滿足大部分的需求了,不過還有一種情況:如果同一布局中存在多個 ComposeView 元素,每個元素必須具有唯一的 ID 才能使 savedInstanceState 發(fā)揮作用:

              
          class ExampleFragment : Fragment() {

            override fun onCreateView(...): View = LinearLayout(...).apply {
                addView(ComposeView(...).apply {
                  id = R.id.compose_view_x
                  ...
                })
                addView(TextView(...))
                addView(ComposeView(...).apply {
                  id = R.id.compose_view_y
                  ...
                })
              }
            }
          }

          上面的代碼也不難,里面需要注意一點,ComposeView ID 需要在 res/values/ids.xml 文件中進(jìn)行定義:

              
          <resources>
              <item name="compose_view_x" type="id" />
              <item name="compose_view_y" type="id" />
          </resources>

          PlayTheme

          上面本來想直接寫主題來著,但是想了想還是說清楚一點吧,要不使用不同場景的就不知道該如何使用了,咱們來接著看剛才定義的 Activity。

          setContent 中包裹了一層 PlayTheme,顧名思義,這是一個自定義的主題,這塊也是顛覆我的一個地方。在我之前對安卓的認(rèn)知中,主題一般存放在 values 中的 styles 文件中,現(xiàn)在 Compose 中已經(jīng)不再使用 xml 中的主題了,取而代之的是 Compose 自己的一套主題系統(tǒng)。在這里我也吃過一次虧:死活修改不了顏色。

          看來 Compose 對 xml 是深惡痛絕啊,一個 xml 都不想使用,包括 color。來看下 PlayTheme 的定義:

              
          @Composable
          fun PlayTheme(
              darkTheme: Boolean = isSystemInDarkTheme()
          ,
              content: @Composable () -> Unit
          ) {
              val colors = if (darkTheme) {
                  PlayThemeDark
              } else {
                  PlayThemeLight
              }
              MaterialTheme(
                  colors = colors,
                  typography = typography,
                  content = content
              )
          }

          private val PlayThemeLight = lightColors(
              primary = blue,
              onPrimary = Color.White,
              primaryVariant = blue,
              secondary = blue
          )

          private val PlayThemeDark = darkColors(
              primary = blueDark,
              onPrimary = Color.White,
              secondary = blueDark,
              surface = blueDark
          )

          定義很簡單,但是根據(jù)上面描述的內(nèi)容發(fā)現(xiàn)了一些什么沒有,連主題都是 Composable ,真的像 Flutter 中的 Widget 。再來看下里面的顏色值:

              
          val blue = Color(0xFF2772F3)
          val blueDark = Color(0xFF0B182E)

          val Purple300 = Color(0xFFCD52FC)
          val Purple700 = Color(0xFF8100EF)

          畫頁面


          準(zhǔn)備工作做得差不多了,來開始畫頁面吧!

          先想想咱們最終需要做的樣子,忘記的可以滑到上面再看看。

          其實很簡單,今天咱們只是初探嘛!只需要畫出下面的底部導(dǎo)航欄和上面幾個空頁面就行了!

          說干就干!先來創(chuàng)建一個新的 Composable:

              
          @Composable
          fun Home() {

          }

          很簡單,一個方法加上 @Composable 的注解就是一個新的 Composable 了,咱們需要在這里畫咱們的首頁了。

          底部導(dǎo)航欄


          查了一下官方文檔, Compose 中和 Flutter 一樣有現(xiàn)成底部導(dǎo)航欄,完全夠咱們使用了:

              
          @Composable
          fun Home() {
              ComposeDemoTheme {
                  val (selectedTab, setSelectedTab) = remember { mutableStateOf(CourseTabs.HOME_PAGE) }
                  val tabs = CourseTabs.values()
                  Scaffold(
                      backgroundColor = MaterialTheme.colors.primarySurface,
                      bottomBar = {
                          BottomNavigation(
                              Modifier.navigationBarsHeight(additional = 56.dp)
                          ) {
                              tabs.forEach { tab ->
                                  BottomNavigationItem(
                                      icon = { Icon(painterResource(tab.icon), contentDescription = null) },
                                      label = { Text(stringResource(tab.title).toUpperCase()) },
                                      selected = tab == selectedTab,
                                      onClick = { setSelectedTab(tab) },
                                      alwaysShowLabel = false,
                                      selectedContentColor = MaterialTheme.colors.secondary,
                                      unselectedContentColor = LocalContentColor.current,
                                      modifier = Modifier.navigationBarsPadding()
                                  )
                              }
                          }
                      }
                  ) { innerPadding ->
                      val modifier = Modifier.padding(innerPadding)
                      when (selectedTab) {
                          CourseTabs.HOME_PAGE -> One(modifier)
                          CourseTabs.PROJECT -> Two(modifier)
                          CourseTabs.OFFICIAL_ACCOUNT -> Three(modifier)
                          CourseTabs.MINE -> Four(modifier)
                      }
                  }
              }
          }

          上面代碼有點長,但是意思很簡單,稍微給大家說一下吧,Scaffold 在 Flutter 中也有,意思也是差不多的,來看一下 Scaffold 的源碼吧:

              
          @Composable
          fun Scaffold(
              modifier: Modifier = Modifier,
              scaffoldState: ScaffoldState = rememberScaffoldState()
          ,
              topBar: @Composable () -> Unit = {},
              bottomBar: @Composable () -> Unit = {},
              snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) },
              floatingActionButton: @Composable () -> Unit = {},
              floatingActionButtonPosition: FabPosition = FabPosition.End,
              isFloatingActionButtonDocked: Boolean = false,
              drawerContent: @Composable (ColumnScope.() -> Unit)? = null,
              drawerGesturesEnabled: Boolean = true,
              drawerShape: Shape = MaterialTheme.shapes.large,
              drawerElevation: Dp = DrawerDefaults.Elevation,
              drawerBackgroundColor: Color = MaterialTheme.colors.surface,
              drawerContentColor: Color = contentColorFor(drawerBackgroundColor),
              drawerScrimColor: Color = DrawerDefaults.scrimColor,
              backgroundColor: Color = MaterialTheme.colors.background,
              contentColor: Color = contentColorFor(backgroundColor),
              content: @Composable (PaddingValues) -> Unit
          )

          它就是一個腳手架,官方是這樣進(jìn)行描述的:

          Material 支持的最高級別的可組合項是 Scaffold。Scaffold 可讓您實現(xiàn)具有基本 Material Design 布局結(jié)構(gòu)的界面。Scaffold 可以為最常見的頂級 Material 組件(如 TopAppBar、BottomAppBar、FloatingActionButton 和 Drawer)提供插槽。通過使用 Scaffold,很容易確保這些組件得到適當(dāng)放置且正確地協(xié)同工作。

          意思很明確,如果不是要求自定義度特別高的頁面,使用 Scaffold 就完全能滿足需求了,這里咱們使用的就是。

          上面代碼中的 CourseTabs 還沒有寫,它是一個枚舉類,用來表示首頁的幾個頁面的:

              
          enum class CourseTabs(
              @StringRes val title: Int,
              @DrawableRes val icon: Int
          ) {
              HOME_PAGE(R.string.home_page, R.drawable.ic_nav_news_normal),
              PROJECT(R.string.project, R.drawable.ic_nav_tweet_normal),
              OFFICIAL_ACCOUNT(R.string.official_account, R.drawable.ic_nav_discover_normal),
              MINE(R.string.mine, R.drawable.ic_nav_my_normal)
          }

          這里其實有的地方大家還是看不太懂的,比如上面的 remember 是個什么東西?

          管理狀態(tài)


          上面所說的的 remember 其實是用來管理 Compose 的狀態(tài)的,大家就先記著 remember 可以記錄咱們點擊的按鈕數(shù)據(jù),從而驅(qū)使頁面發(fā)生改變吧。

          mutableStateOf(CourseTabs.HOME_PAGE)  其實是 MutableState<CourseTabs> 

          [mutableStateOf]:


          https://developer.android.google.cn/reference/kotlin/androidx/compose/runtime/package-summary#mutableStateOf(androidx.compose.runtime.mutableStateOf.T



          androidx.compose.runtime.SnapshotMutationPolicy)) 會創(chuàng)建可觀察的 MutableState, MutableState是與 Compose 運行時集成的可觀察類型。

          這塊一時半會說不清,需要后面的文章慢慢來和大家講。

          添加頁面


          上面一共創(chuàng)建了四個字頁面:one、two、three、four,四個頁面非常簡單:

              
          @Composable
          fun One(modifier: Modifier) {
              Text(modifier = modifier
                  .fillMaxSize()
                  .padding(top = 100.dp), text = "One", color = Teal200)
          }

          @Composable
          fun Two(modifier: Modifier) {
              Text(modifier = modifier
                  .fillMaxSize()
                  .padding(top = 100.dp), text = "Two", color = Teal200)
          }

          @Composable
          fun Three(modifier: Modifier) {
              Text(modifier = modifier
                  .fillMaxSize()
                  .padding(top = 100.dp), text = "Three", color = Teal200)
          }

          @Composable
          fun Four(modifier: Modifier) {
              Text(modifier = modifier
                  .fillMaxSize()
                  .padding(top = 100.dp), text = "Four", color = Teal200)
          }

          ok,這就差不多了。

          總結(jié)


          本篇文章就先寫到這里吧,本篇簡單介紹了下 Compose,并編寫了一個簡單的頁面,之后會將整個應(yīng)用全部使用 Compose 編寫完成的。

          最后再放一下 Github 地址吧,別忘了切換 main 分支!

          Github 地址:


          https://github.com/zhujiang521/PlayAndroid


          ·················END·················

          推薦閱讀

          ? 耗時2年,Android進(jìn)階三部曲第三部《Android進(jìn)階指北》出版!

          ? 『BATcoder』做了多年安卓還沒編譯過源碼?一個視頻帶你玩轉(zhuǎn)!

          ? 『BATcoder』我去!安裝Ubuntu還有坑?

          ? 重生!進(jìn)階三部曲第一部《Android進(jìn)階之光》第2版 出版!

          BATcoder技術(shù)群,讓一部分人先進(jìn)大廠

          大家,我是劉望舒,騰訊TVP,著有三本技術(shù)暢銷書,連續(xù)四年蟬聯(lián)電子工業(yè)出版社年度優(yōu)秀作者,谷歌開發(fā)者社區(qū)特邀講師,百度百科收錄的p8+級專家。

          前華為技術(shù)專家,現(xiàn)大廠技術(shù)負(fù)責(zé)人。

          想要加入 BATcoder技術(shù)群,公號回復(fù)BAT 即可。

          為了防止失聯(lián),歡迎關(guān)注我的小號


                          
            微信改了推送機制,真愛請星標(biāo)本公號??
          瀏覽 105
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  淫五月亭亭六月丁香 | 国产无码精品黄色电影 | 中文字幕一区二区久久人妻 | 免费一区区三区四区 | 国产黄色电影网址 |