【建議收藏】Jetpack Compose編程知識全彙總 (含詳細實例講解)

1、簡述

Jetpack Compose是Google I/O 2019 發佈的Andorid UI框架,它不一樣於Andorid常見的Xml+命令式Coding的UI開發範式,而是基於Kotlin的DSL實現了一套相似React的聲明式UI框架。Jetpack Compose目前仍然處於Alpha版本,目標是2020年可以發佈穩定的Beta版本。伴隨React Native、Flutter等大前端框架的興起以及Jetpack Compose、SwiftUI等native框架的出現,聲明式UI正逐漸成爲客戶端UI開發的新趨勢。前端

特色:java

  1. 聲明式編程模型,界面隨應用狀態自動更新
  2. 組合 vs 繼承
  3. 關注點分離(SOC),減小耦合,增長內聚
  4. 更少的代碼,Kotlin簡潔且易維護
  5. 快速的開發,支持實時預覽界面,並支持互動式預覽
  6. 向後兼容,與現有視圖共同使用,無縫連接,並支持Material Design和動畫

2、環境配置

因爲Jetpack Compose還未正式發佈,須要下載最新Canary版的Android Studio 預覽版node

如下三種方式可初步體驗:python

  1. 嘗試使用Jetpack Compose 示例應用
  2. 建立支持Jetpack Compose 的新應用
  3. 現有項目中支持Jetpack Compose

基於現狀,我主要介紹第三種方式:android

  • 配置Kotlingit

    plugins {  
      id 'org.jetbrains.kotlin.android' 
      version '1.4.0' 
    }
    複製代碼
  • 配置Gradlegithub

    android {    
      defaultConfig {        
        ...        
          minSdkVersion 21    
      }     
      buildFeatures {        
        // Enables Jetpack Compose for this module 
        compose true    
      }    ...     
        // Set both the Java and Kotlin compilers to target Java 8. 
        compileOptions {        
          sourceCompatibility 
          JavaVersion.VERSION_1_8        
          targetCompatibility JavaVersion.VERSION_1_8    
        }     
      kotlinOptions {        
        jvmTarget = "1.8"        
        useIR = true    
      }     
      composeOptions {        
        kotlinCompilerVersion '1.4.0'        
        kotlinCompilerExtensionVersion '1.0.0-alpha05'    
      } 
    }
    複製代碼
  • 添加工具包依賴項編程

    dependencies {    
      implementation 'androidx.compose.ui:ui:1.0.0-alpha05'    
      // Tooling support (Previews, etc.) 
      implementation 'androidx.ui:ui-tooling:1.0.0-alpha05'    
      // Foundation (Border, Background, Box, Image, Scroll, shapes, animations, etc.) 
      implementation 'androidx.compose.foundation:foundation:1.0.0-alpha05'    
      // Material Design 
      implementation 'androidx.compose.material:material:1.0.0-alpha05'    
      // Material design icons 
      implementation 'androidx.compose.material:material-icons-core:1.0.0-alpha05'    
      implementation 'androidx.compose.material:material-icons-extended:1.0.0-alpha05'    
      // Integration with observables 
      implementation 'androidx.compose.runtime:runtime-livedata:1.0.0-alpha05'    
      implementation 'androidx.compose.runtime:runtime-rxjava2:1.0.0-alpha05'     
      // UI Tests androidTestImplementation 'androidx.ui:ui-test:1.0.0-alpha05' }
    複製代碼

3、基礎使用

​ Jetpack Compose包含了基本組件(compose.ui)、Material Design 組件(compose.material)、動畫組件(compose.animation)等衆多UI組件,在此我就不贅述了,在對應的文檔中你們均可以參閱,此處我重點講解一下關於Compose的關鍵點緩存

@Compose

全部關於構建View的方法都必須添加@Compose的註解才能夠。而且@Compose跟協程的Suspend的使用方法比較相似,被@Compose的註解的方法只能在一樣被@Comopse註解的方法中才能被調用。前端框架

@Composable 
fun Greeting(name: String) {    
  Text(text = "Hello $name!") 
}
複製代碼

@Preview

經常使用用參數以下:

  1. name: String: 爲該Preview命名,該名字會在佈局預覽中顯示。
  2. showBackground: Boolean: 是否顯示背景,true爲顯示。
  3. backgroundColor: Long: 設置背景的顏色。
  4. showDecoration: Boolean: 是否顯示Statusbar和Toolbar,true爲顯示。
  5. group: String: 爲該Preview設置group名字,能夠在UI中以group爲單位顯示。
  6. fontScale: Float: 能夠在預覽中對字體放大,範圍是從0.01。
  7. widthDp: Int: 在Compose中渲染的最大寬度,單位爲dp。
  8. heightDp: Int: 在Compose中渲染的最大高度,單位爲dp。

上面的參數都是可選參數,還有像背景設置等的參數並非對實際的App進行設置,只是對Preview中的背景進行設置,爲了更容易看清佈局。

@Preview(showBackground = true) 
@Composable fun DefaultPreview() {    
  ComposeDemoTheme {        
    Greeting("Android")    
  } 
}
複製代碼

當更改跟UI相關的代碼時,會顯示以下圖的一個橫條通知,點擊Build&Refresh便可更新顯示所更改代碼的UI。

img

setContent

該方法做用是和zaiLayout/View中的setContentView是同樣的。setContent的方法也是有@Compose註解的方法。因此,在setContent中寫入關於UI的@Compopse方法,便可在Activity中顯示。

class MainActivity : AppCompatActivity() {    
  override fun onCreate(savedInstanceState: Bundle?) {        
  super.onCreate(savedInstanceState)        
    setContent {            
      Text("Hello world!")        
    } 
}
複製代碼

Modifier

該類是各個Compose的UI組件必定會用到的一個類。它是被用於設置UI的擺放位置,padding等修飾信息的類。關於Modifier相關的設置較多,在這裏只介紹會常常用到的。

Modifier.padding(10.dp) // 給上下左右設置成同一個值 
Modifier.padding(10.dp, 11.dp, 12.dp, 13.dp) // 分別爲上下左右設值 
Modifier.padding(10.dp, 11.dp) // 分別爲上下和左右設值 
Modifier.padding(InnerPadding(10.dp, 11.dp, 12.dp, 13.dp))// 分別爲上下左右設值 
Modifier.fillMaxHeight() // 填充整個寬度 
Modifier.fillMaxHeight() // 填充整個高度 
Modifier.fillMaxSize() // 填充整個寬度和高度 
Modifier.width(2.dp) // 設置寬度 
Modifier.height(3.dp)  // 設置高度 
Modifier.size(4.dp, 5.dp) // 設置高度和寬度 
Modifier.widthIn(2.dp) // 設置最大寬度 
Modifier.heightIn(3.dp) // 設置最大高度 
Modifier.sizeIn(4.dp, 5.dp, 6.dp, 7.dp) // 設置最大最小的寬度和高度
Modifier.gravity(Alignment.CenterHorizontally) // 橫向居中 
Modifier.gravity(Alignment.Start) // 橫向居左 
Modifier.gravity(Alignment.End) // 橫向居右 
Modifier.rtl  // 從右到左 Modifier.ltr // 從左到右 
Modifier.plus(otherModifier) // 把otherModifier的信息加入到現有的modifier中 
// Modifier的方法都返回Modifier的實例的鏈式調用,因此只要連續調用想要使用的方法便可。 
@Composable fun Greeting(name: String) {    
  Text(text = "Hello $name!", modifier = Modifier.padding(20.dp).fillMaxWidth()) 
}
複製代碼

Column,Row

Column和Row能夠理解爲在View/Layout體系中的縱向和橫向的ViewGroup

Column( 
  verticalArrangement:Arrangement // 控制縱向佈局關係 
  horizontalAlignment:Alignment // 控制橫向對齊關係 
) 

Row( 
  horizontalArrangement:Alignment // 控制橫向佈局關係 
  verticalAlignment:Arrangement // 控制縱向對齊關係
)  

@Composable 
fun TestColumnRow() {    
  Column(        
    modifier = Modifier.fillMaxHeight().background(Color.Yellow),              
    verticalArrangement = Arrangement.SpaceBetween,        
    horizontalAlignment = Alignment.Start    
  ) {        
    Text(text = "java")        
    Text(text = "android")        
    Text(text = "python")    
  }     
  Row(        
    modifier = Modifier.fillMaxWidth().background(Color.LightGray),        
    verticalAlignment = Alignment.Top,        
    horizontalArrangement = Arrangement.SpaceBetween    
  ) {        
    Text(text = "java")        
    Text(text = "android")        
    Text(text = "python")    
  } 
} 
複製代碼

4、進階使用

狀態管理

全部 Android 應用都有核心界面更新循環,以下所示:

img

Compose 專爲單向數據流而打造。這是一種狀態向下流動而事件向上流動的設計。

img

使用單向數據流的應用的界面更新循環以下所示:

事件:事件由界面的一部分生成而且向上傳遞。

更新狀態:事件處理腳本能夠更改狀態。

顯示狀態:狀態會向下傳遞,界面會觀察新狀態並顯示該狀態。

舉兩個例子展現:

//內部狀態管理 
@Composable 
fun CounterInner() {    
  val count = remember { mutableStateOf(0) }    
  Button(onClick = { count.value += 1 }) 
  {        
    Text(text = "Count: ${count.value}")    
  } 
}
複製代碼

解釋一下上圖的數據流狀況

事件:當點擊發生時候,會觸發count.value

更新狀態:mutableStateOf會進行處理,而後設置count的狀態

顯示狀態:系統會調用count的觀察器,而且界面會顯示新狀態

//支持其餘可觀察類型的狀態管理 
class CountViewModel : ViewModel() {    
  // LiveData holds state which is observed by the UI 
  // (state flows down from ViewModel) 
  private val _count = MutableLiveData(0)   
  val count: LiveData<Int> = _count     
  // onNameChanged is an event we're defining that the UI can invoke 
  // (events flow up from UI) 
  fun onCountChanged(newCount: Int) {        
    _count.value = newCount    
  } 
} 

@Composable 
fun Counter(countViewModel: CountViewModel = viewModel()) {     
  val observeAsState = countViewModel.count.observeAsState(0)    
  val count = observeAsState.value     
  Button(        
    colors = ButtonConstants.defaultButtonColors(backgroundColor = if (count > 5) Color.Green else Color.White),        
    onClick = { countViewModel.onCountChanged(count + 1) },    
  ) {        
    Text(text = "I've been clicked $count times")    
  } 
}
複製代碼

解釋一下上圖的數據流狀況

事件:當點擊發生時候,會觸發onCountChanged

更新狀態:onCountChanged會進行處理,而後設置_count的狀態

顯示狀態:系統會調用count的觀察器,而且界面會顯示新狀態

狀態提高

  • 無狀態可組合項是指自己沒法改變任何狀態的可組合項。無狀態組件更容易測試、發生的錯誤每每更少,而且更有可能重複使用。
  • 若是您的可組合項有狀態,您能夠經過使用狀態提高使其變爲無狀態。
  • 狀態提高是一種編程模式,在這種模式下,經過將可組合項中的內部狀態替換爲參數和事件,將狀態移至可組合項的調用方。
  • 狀態提高的過程可以讓您將單向數據流擴展到無狀態可組合項。在這些可組合項的單向數據流示意圖中,隨着更多可組合項與狀態交互,狀態仍向下流動,而事件向上流動。
@Composable 
fun Counter(countViewModel: CountViewModel = viewModel()) {     
  val observeAsState = countViewModel.count.observeAsState(0)    
  val count = observeAsState.value        
  ButtonCount(count = count, onCountChanged = { countViewModel.onCountChanged(it) }) 
} 

@Composable 
fun ButtonCount( /* state */ count: Int, /* event */ onCountChanged: (Int) -> Unit ) {    
  Button(        
    colors = ButtonConstants.defaultButtonColors(backgroundColor = if (count > 5) Color.Green else Color.White),        
    onClick = { onCountChanged(count + 1) },    
  ) {        
    Text(text = "I've been clicked $count times")    
  } 
}
複製代碼

互操做

Android View中的Compose

若是想使用Compose的狀況下,又不想遷移整個應用,能夠在xml裏面增長ComposeView,相似於佔位符,而後在Actviity/fragment中尋找該控件並調用setContent方法便可,在該方法中便可使用compose相關屬性

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".AndroidViewComposeActivity">     
  <TextView android:id="@+id/hello_world" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Hello Android!" app:layout_constraintTop_toTopOf="parent" />     
  <androidx.compose.ui.platform.ComposeView android:id="@+id/compose_view_text" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@id/hello_world" />     
  <androidx.compose.ui.platform.ComposeView android:id="@+id/compose_view_img" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@id/compose_view_text" />
</androidx.constraintlayout.widget.ConstraintLayout>
複製代碼
class AndroidViewComposeActivity : AppCompatActivity() {    
  override fun onCreate(savedInstanceState: Bundle?) {        
    super.onCreate(savedInstanceState)        
    setContentView(R.layout.activity_android_view_compose)         
    findViewById<ComposeView>(R.id.compose_view_text).setContent {            
      MaterialTheme {                
        Text("Hello Compose!")            
      }        
    }         
    findViewById<ComposeView>(R.id.compose_view_img).setContent {            
      val imageResource = imageResource(id = R.drawable.header)            
      val imageModifier = Modifier.preferredHeight(180.dp)
      .fillMaxWidth()
      .padding(16.dp)
      .clip(RoundedCornerShape(4.dp))
      
      MaterialTheme {
        Image(                    
          bitmap = imageResource,                    
          modifier = imageModifier,                    
          contentScale = ContentScale.Crop                
        )            
      }        
    }    
  } 
}
複製代碼
Compose中的Android View

若是碰到在Compose環境中,想要使用Android的View視圖的狀況,只須要使用AndroidView函數便可

@Composable 
fun CustomView() {    
  val selectedItem = remember { mutableStateOf(0) }     
  val context = AmbientContext.current     
  val customView = remember {        
    // Creates custom view 
    Button(context).apply {            
      // Sets up listeners for View -> Compose communication 
      setOnClickListener {                
        selectedItem.value += 1            
      }        
    }   
  }     
  // Adds view to Compose 
  AndroidView({ customView }) { 
    view ->
    // View's been inflated - add logic here if necessary 
    // As selectedItem is read here, AndroidView will recompose 
    // whenever the state changes 
    // Example of Compose -> View communication 
    view.text = selectedItem.value.toString()    
  } 
}
複製代碼

若是是須要使用xml的配置狀況,也使用AndroidView函數便可

@Composable 
fun CustomView2() {    
  val context = AmbientContext.current     
  val customView = remember {        
    // Creates custom view 
    View.inflate(context, R.layout.layout_custom_view, null)    
  }     
  AndroidView({ customView }) 
}
複製代碼

與通用庫集成

ViewModel

從源碼可看出,viewmodel函數底層也是經過ViewModelProvider進行獲取的

@Composable 
fun <VM : ViewModel> viewModel( modelClass: Class<VM>, key: String? = null, factory: ViewModelProvider.Factory? = null )
: VM = AmbientViewModelStoreOwner.current.get(modelClass, key, factory)
複製代碼
數據流

Compose也是適配Android主流的基於流的方案,如

  • LiveData.observeAsState()
  • Flow.collectAsState()
  • Observable.subscribeAsState()

在Compose中,LiveData.observeAsState()獲取的State對象賦值給Text

@Composable 
fun HelloScreen(helloViewModel: HelloViewModel = viewModel()) {    
  // by default, viewModel() follows the Lifecycle as the Activity or Fragment 
  // that calls HelloScreen(). This lifecycle can be modified by callers of HelloScreen. 
  // name is the _current_ value of [helloViewModel.name] 
  // with an initial value of "" 
  val observeAsState = helloViewModel.name.observeAsState("")     
  Column {        
    Text(text = observeAsState.value)        
    TextField(            
      value = observeAsState.value,            
      onValueChange = { helloViewModel.onNameChanged(it) },            
      label = { Text("Name") }        
    )    
  } 
}
複製代碼
異步操做

此處須要補充說明的是Compose的生命週期

Compose經過一系列Effect方法,實現生命週期函數

Compose生命週期 說明 對應React
onActive compose函數第一次被渲染到畫面 componentWillMount componentDidMount
onDispose compose函數從畫面上移除 componentWillUnmount
onCommit compose函數每次執行,畫面從新渲染 componentDidUpdate

因此onCommit函數的使用相似於React的useEffect,支持可觀察函數

@Suppress("ComposableNaming") 
@Composable 
/*inline*/ 
fun </*reified*/ V1> onCommit( v1: V1, /*noinline*/ callback: CommitScope.() -> Unit ) {    
  remember(v1) { PreCommitScopeImpl(callback) } 
}
複製代碼

僅當v1發生變化時onCommit纔會執行

舉個例子使用異步操做

@Composable fun fetchImage(url: String): ImageAsset? {    
  // Holds our current image, and will be updated by the onCommit lambda below 
  var image by remember(url) { mutableStateOf<ImageAsset?>(null) }     
  onCommit(url) {        
    // This onCommit lambda will be invoked every time url changes 
    val listener = object : ExampleImageLoader.Listener() {            
      override fun onSuccess(bitmap: Bitmap) {                
        // When the image successfully loads, update our image state 
        image = bitmap.asImageAsset()            
      }        
    }         
    // Now execute the image loader 
    val imageLoader = ExampleImageLoader.get()        
    imageLoader.load(url).into(listener)         
    onDispose {            
      // If we leave composition, cancel any pending requests 
      imageLoader.cancel(listener)        
    }    
  }     
  // Return the state-backed image property. Any callers of this function 
  // will be recomposed once the image finishes loading 
  return image 
}
複製代碼

5、原理解析

由於代碼是基於Kotlin註解動態生成的,查看方法能夠先build一個apk,而後查看其中的classess.dex文件,使用dex2jar轉爲jar包,而後使用jd-gui進行查看,下圖是反編譯獲得的源碼

//CountActivityKt.class->CountActivity->CounterInner(Composer,int):void 
public static final void CounterInner(Composer<?> paramComposer, int paramInt) {
 paramComposer.startRestartGroup(-908461591,"C(CounterInner)47@2322L30,48@2374L20,48@2357L91:CountActivity.kt#ffoge4");    
  if (paramInt != 0 || !paramComposer.getSkipping()) {      
    paramComposer.startReplaceableGroup(-3687207, "C(remember):Remember.kt#9igjgp");      
    Object object = paramComposer.nextSlot();      
    if (object == SlotTableKt.getEMPTY()) {        
      object = MutableStateKt.mutableStateOf$default(Integer.valueOf(LiveLiterals$CountActivityKt.INSTANCE.Int$arg-0$call-mutableStateOf$fun-$anonymous$$arg-0$call-remember$val-count$fun-CounterInner()), null, 2, null);        paramComposer.updateValue(object);      
    }       
    paramComposer.endReplaceableGroup();      
    MutableState<Integer> mutableState = (MutableState)object;   paramComposer.startReplaceableGroup(-3686846, "C(remember)P(1):Remember.kt#9igjgp");      
    boolean bool = paramComposer.changed(mutableState);      
    object = paramComposer.nextSlot();      
    if (object == SlotTableKt.getEMPTY() || bool) {        
      object = new CountActivityKt$CounterInner$1$1(mutableState);        
      paramComposer.updateValue(object);      
    }       
    paramComposer.endReplaceableGroup();      
    ButtonKt
      .Button((Function0)object, null, false, null, null, null, null, null, null,(Function3)ComposableLambdaKt.composableLambda(paramComposer, -819892270, true, "C49@2406L36:CountActivity.kt#ffoge4",                                                                                                                         new CountActivityKt$CounterInner$2(mutableState)), paramComposer, 805306368, 510);    
  	} else {      
    paramComposer.skipToGroupEnd();    
  	}     
  	ScopeUpdateScope scopeUpdateScope = paramComposer.endRestartGroup();    
  if (scopeUpdateScope == null)      
    return;     
  scopeUpdateScope.updateScope(new CountActivityKt$CounterInner$3(paramInt));  
}
複製代碼

仔細查看源碼可知

  1. Composeable Annotation:
    1. 當編譯器看到Composeable註解時,會插入額外的參數和函數調用等模板代碼,
    2. 其中頭部會加入startRestartGroup,尾部會加入endRestartGroup,中部函數部分會加入分組信息(startReplaceableGroup,endReplaceableGroup)
    3. 底層是經過Gap Buffer的方式進行Layoutnode的複用和管理
  2. 位置記憶化:
    1. 執行時候會記憶代碼執行順序及緩存每一個節點
    2. 打下分組信息(startReplaceableGroup,endReplaceableGroup),以組別進行更新
  3. 重組:
    1. 獲取到可組合函數(State),並與當前方法的Composer對象進行綁定
    2. 將狀態保管到Composer內部的槽表中進行管理
    3. 內部的layoutnode複用和管理經過Gap Buffer方式進行

6、附錄材料