|
| 1 | += Introduction to Jetpack Compose for NativeScript |
| 2 | + |
| 3 | +Building user interfaces declaratively is something the Web community has widely adopted, and nowadays, large applications are built following these principles. For example, Google launched Jetpack Compose, and Apple announced SwiftUI at WWDC19, receiving an immensely positive response from developers. |
| 4 | + |
| 5 | +Here at Valor Software, we are always excited about new advancements in development technologies, and we are fans of NativeScript. We collaborated with nStudio to provide an effective and enjoyable Jetpack Compose integration for Android apps driven by NativeScript. |
| 6 | + |
| 7 | +Earlier this month we announced https://dev.to/valorsoftware/introduction-to-swiftui-for-nativescript-4m1b[SwiftUI for NativeScript, window=_blank], which follow the same principles and API design as Jetpack Compose for NativeScript. |
| 8 | +
|
| 9 | +In this article, we'll demonstrate how to use Jetpack Compose within NativeScript to explore fun new possibilities in building amazing UIs together. |
| 10 | +
|
| 11 | +== Create a NativeScript app |
| 12 | +
|
| 13 | +We can create an app using a standard TypeScript template: |
| 14 | +
|
| 15 | +[, bash] |
| 16 | +---- |
| 17 | +ns create jetpackcompose --ts |
| 18 | +cd jetpackcompose |
| 19 | +---- |
| 20 | +
|
| 21 | +This will setup what is often called a "vanilla" flavored NativeScript app. You can use whichever flavor you're most comfortable with, though. Setting the plugin up for Angular (and most other flavors) is usually a case of registering the view, which we'll demonstrate in a section below. |
| 22 | +
|
| 23 | +== Install the Jetpack Compose plugin: |
| 24 | +
|
| 25 | +[, bash] |
| 26 | +---- |
| 27 | +npm install @nativescript/jetpack-compose |
| 28 | +---- |
| 29 | +
|
| 30 | +NOTE: Jetpack Compose requires you to use at least API 21 (Lollipop) as your minimum SDK version. You can do this by adding `minSdkVersion 21` to your app.gradle. |
| 31 | +
|
| 32 | +If you plan to build your libraries directly from Android Studio, you don't need anything else, just drop your built `.aar` in `App_Resources/Android/libs/` and skip to the next section. But if you're planning on writing Kotlin code directly in `.kt` files in `App_Resources/Android/src/main/java`, then we need some extra steps. |
| 33 | +
|
| 34 | +First, add your compose dependencies in `app.gradle`: |
| 35 | +
|
| 36 | +[, json] |
| 37 | +---- |
| 38 | +dependencies { |
| 39 | + def compose_version = "1.2.1" |
| 40 | + implementation "androidx.compose.ui:ui:$compose_version" |
| 41 | + // Tooling support (Previews, etc.) |
| 42 | + implementation "androidx.compose.ui:ui-tooling:$compose_version" |
| 43 | +
|
| 44 | + // Add any other dependencies your Jetpack Compose UI needs |
| 45 | + // like material design: |
| 46 | + // implementation 'androidx.compose.material:material:$compose_version' |
| 47 | +} |
| 48 | +---- |
| 49 | +
|
| 50 | +Then modify the `android` section so you enable compose: |
| 51 | +
|
| 52 | +[, json] |
| 53 | +---- |
| 54 | +android { |
| 55 | + // other settings like targetSdk, etc. |
| 56 | +
|
| 57 | + buildFeatures { |
| 58 | + compose true |
| 59 | + } |
| 60 | + compileOptions { |
| 61 | + sourceCompatibility JavaVersion.VERSION_1_8 |
| 62 | + targetCompatibility JavaVersion.VERSION_1_8 |
| 63 | + } |
| 64 | + kotlinOptions { |
| 65 | + jvmTarget = "1.8" |
| 66 | + } |
| 67 | + composeOptions { |
| 68 | + kotlinCompilerExtensionVersion '1.3.2' |
| 69 | + } |
| 70 | +} |
| 71 | +---- |
| 72 | +
|
| 73 | +And finally, enable Kotlin by creating the file `App_Resources/Android/gradle.properties` |
| 74 | +
|
| 75 | +[, json ] |
| 76 | +---- |
| 77 | +useKotlin=true |
| 78 | +kotlinVersion=1.7.20 # you can choose your kotlin version here |
| 79 | +---- |
| 80 | +
|
| 81 | +== Jetpack Compose usage |
| 82 | +
|
| 83 | +A. Create your Jetpack Compose views and wrapper |
| 84 | ++ |
| 85 | +Create `App_Resources/Android/src/main/java/BasicView.kt`: |
| 86 | ++ |
| 87 | +[, java] |
| 88 | +---- |
| 89 | +package com.example |
| 90 | +
|
| 91 | +import android.content.Context |
| 92 | +import androidx.compose.material.MaterialTheme |
| 93 | +import androidx.compose.material.Text |
| 94 | +import androidx.compose.runtime.Composable |
| 95 | +import androidx.compose.runtime.getValue |
| 96 | +import androidx.compose.runtime.mutableStateOf |
| 97 | +import androidx.compose.runtime.setValue |
| 98 | +import androidx.compose.ui.platform.ComposeView |
| 99 | +import androidx.lifecycle.ViewModel |
| 100 | +import androidx.lifecycle.viewmodel.compose.viewModel |
| 101 | +
|
| 102 | +class BasicView { |
| 103 | + fun generateComposeView(view: ComposeView): ComposeView { |
| 104 | + return view.apply { |
| 105 | + setContent { |
| 106 | + MaterialTheme { |
| 107 | + Text("Hello from Jetpack Compose") |
| 108 | + } |
| 109 | + } |
| 110 | + } |
| 111 | + } |
| 112 | +
|
| 113 | + fun updateData(value: Map<Any, Any>) { |
| 114 | + } |
| 115 | + var onEvent: ((String) -> Unit)? = null |
| 116 | +
|
| 117 | +} |
| 118 | +---- |
| 119 | ++ |
| 120 | +To use the default plugin handling of Compose views, it's important that your implementation follows the following interface: |
| 121 | ++ |
| 122 | +[, java] |
| 123 | +---- |
| 124 | +class Example { |
| 125 | + fun generateComposeView(view: ComposeView): ComposeView { |
| 126 | + // render your compose views into the ComposeView |
| 127 | + } |
| 128 | +
|
| 129 | + fun updateData(value: Map<Any, Any>) { |
| 130 | + // this function receives data from NativeScript |
| 131 | + // value is a js object converted to a map |
| 132 | + } |
| 133 | +
|
| 134 | + // this is the event you will send back to Jetpack Compose |
| 135 | + // when you need to pass data, just call onEvent?.invoke(v) |
| 136 | + var onEvent: ((Any) -> Unit)? = null |
| 137 | +
|
| 138 | +} |
| 139 | +---- |
| 140 | +
|
| 141 | +B. Register your Jetpack Compose via the `composeId` |
| 142 | ++ |
| 143 | +This can be done in the NativeScript app's bootstrap file (often `app.ts` or `main.ts`). |
| 144 | ++ |
| 145 | +[, js] |
| 146 | +---- |
| 147 | +import { registerJetpackCompose, ComposeDataDriver } from '@nativescript/jetpack-compose'; |
| 148 | +
|
| 149 | +// A. You can generate types for your own Compose Provider with 'ns typings android --aar {path/to/{name}.aar}' |
| 150 | +// B. Otherwise you can ignore by declaring the package resolution path you know you provided |
| 151 | +declare var com; |
| 152 | +registerJetpackCompose('sampleView', (view) => new ComposeDataDriver(new com.example.BasicView(), view)); |
| 153 | +---- |
| 154 | ++ |
| 155 | +Additionally, if you want to use Angular, you can register the compose view itself: |
| 156 | ++ |
| 157 | +[, js] |
| 158 | +---- |
| 159 | +import { registerElement } from '@nativescript/angular'; |
| 160 | +import { JetpackCompose } from '@nativescript/jetpack-compose'; |
| 161 | +
|
| 162 | +registerElement('JetpackCompose', () => JetpackCompose) |
| 163 | +---- |
| 164 | +
|
| 165 | +C. Insert into any NativeScript layout |
| 166 | +
|
| 167 | +`app/main-page.xml` |
| 168 | +
|
| 169 | +[, xml] |
| 170 | +---- |
| 171 | +<Page |
| 172 | + xmlns="http://schemas.nativescript.org/tns.xsd" |
| 173 | + xmlns:jc="@nativescript/jetpack-compose" |
| 174 | + class="page"> |
| 175 | + <StackLayout> |
| 176 | + <jc:JetpackCompose composeId="sampleView" height="100" /> |
| 177 | + </StackLayout> |
| 178 | +</Page> |
| 179 | +---- |
| 180 | +
|
| 181 | +You can now run the app with `ns debug android`. |
| 182 | +
|
| 183 | +== Use Android Studio to develop and preview Jetpack Compose |
| 184 | +
|
| 185 | +After running the app once you can open the `platforms/android` folder in Android Studio where you'll be able to find the `BasicView.kt` file. From there you can start modifying it and previewing your changes (by adding the `@Preview` decorator on the `@Composable` you want to preview). |
| 186 | +
|
| 187 | +IMPORTANT: Saving this file will not change the BasicView.kt that lives inside your App_Resources, so be VERY careful to copy the file contents back once you're done editing it! This will become a DX improvement in the future. |
| 188 | +
|
| 189 | +Alternatively, you can create a https://proandroiddev.com/create-an-android-library-aar-79d2338678ba[new Android library, window=_blank] and develop all your Jetpack Compose views there. |
| 190 | +
|
| 191 | +== Sending and receiving data to/from NativeScript |
| 192 | +
|
| 193 | +First, let's add some bindings to our BasicView so it now receives data in `updateData` and displays that, as well as output an event once the data is updated: |
| 194 | +
|
| 195 | +[, js] |
| 196 | +---- |
| 197 | +package com.example |
| 198 | +
|
| 199 | +import android.content.Context |
| 200 | +import androidx.compose.material.MaterialTheme |
| 201 | +import androidx.compose.material.Text |
| 202 | +import androidx.compose.runtime.Composable |
| 203 | +import androidx.compose.runtime.getValue |
| 204 | +import androidx.compose.runtime.mutableStateOf |
| 205 | +import androidx.compose.runtime.setValue |
| 206 | +import androidx.compose.ui.platform.ComposeView |
| 207 | +import androidx.lifecycle.ViewModel |
| 208 | +import androidx.lifecycle.viewmodel.compose.viewModel |
| 209 | +
|
| 210 | +class BasicView { |
| 211 | + data class ExampleUiState( |
| 212 | + val text: String = "" |
| 213 | + ) {} |
| 214 | + class ExampleViewModel( |
| 215 | + ) : ViewModel() { |
| 216 | +
|
| 217 | + var uiState by mutableStateOf(ExampleUiState()) |
| 218 | + } |
| 219 | +
|
| 220 | + var mViewModel = ExampleViewModel() |
| 221 | + fun generateComposeView(view: ComposeView): ComposeView { |
| 222 | +
|
| 223 | + return view.apply { |
| 224 | + setContent { |
| 225 | + MaterialTheme { |
| 226 | +
|
| 227 | + val uiState = mViewModel.uiState; |
| 228 | + // In Compose world |
| 229 | + Text(uiState.text) |
| 230 | + } |
| 231 | + } |
| 232 | + } |
| 233 | + } |
| 234 | +
|
| 235 | + fun updateData(value: Map<Any, Any>) { |
| 236 | + val v = value["data"] as String; |
| 237 | + onEvent?.invoke(v) |
| 238 | + mViewModel.uiState = ExampleUiState(v); |
| 239 | + } |
| 240 | +
|
| 241 | + var onEvent: ((String) -> Unit)? = null |
| 242 | +
|
| 243 | +} |
| 244 | +---- |
| 245 | +
|
| 246 | +== Use your Jetpack Compose in a NativeScript layout |
| 247 | +
|
| 248 | +`app/main-page.xml:` |
| 249 | +
|
| 250 | +[, xml] |
| 251 | +---- |
| 252 | +<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" class="page" |
| 253 | + xmlns:jc="@nativescript/jetpack-compose"> |
| 254 | + <StackLayout> |
| 255 | + <Label text="The following view is Jetpack Compose inside NativeScript!" textWrap="true"></Label> |
| 256 | + <jc:JetpackCompose composeEvent="{{ onEvent }}" data="{{ text }}" composeId="sampleView"></sw:JetpackCompose> |
| 257 | + <Label text="This is NativeScript again"></Label> |
| 258 | + <TextView textChange="{{ onTextChange }}" text="{{ text }}" textWrap="true"></TextView> |
| 259 | + </StackLayout> |
| 260 | +</Page> |
| 261 | +---- |
| 262 | +
|
| 263 | +`app/main-page.ts:` |
| 264 | +
|
| 265 | +[, js] |
| 266 | +---- |
| 267 | +import { Observable } from '@nativescript/core'; |
| 268 | +import { registerJetpackCompose, ComposeDataDriver } from '@nativescript/jetpack-compose'; |
| 269 | +import { EventData, Page, PropertyChangeData } from '@nativescript/core'; |
| 270 | +
|
| 271 | +// A. You can generate types for your own Compose Provider with 'ns typings android --aar {path/to/{name}.aar}' |
| 272 | +// B. Otherwise you can ignore by declaring the package resolution path you know you provided |
| 273 | +declare var com; |
| 274 | +registerJetpackCompose('sampleView', (view) => new ComposeDataDriver(new com.example.BasicView(), view)); |
| 275 | +
|
| 276 | +export function navigatingTo(args: EventData) { |
| 277 | + const page = <Page>args.object; |
| 278 | + page.bindingContext = new DemoModel(); |
| 279 | +} |
| 280 | +
|
| 281 | +export class DemoModel extends Observable { |
| 282 | + text = ''; |
| 283 | +
|
| 284 | + onEvent(evt: JetpackComposeEventData<string>) { |
| 285 | + console.log('onEvent', evt.data); |
| 286 | + } |
| 287 | +
|
| 288 | + onTextChange(evt: PropertyChangeData) { |
| 289 | + console.log('textChange', evt.value); |
| 290 | + this.set('text', evt.value); |
| 291 | + } |
| 292 | +} |
| 293 | +---- |
| 294 | +
|
| 295 | +Now every time you change the text on the NativeScript `TextView` it'll update the text on the Jetpack Compose view! |
| 296 | +
|
| 297 | +video::s_Q3gQz_Tqo[Use Jetpack Compose with NativeScript via @nativescript/jetpack-compose with data bindings.] |
| 298 | +
|
| 299 | +== ColorPicker example |
| 300 | +
|
| 301 | +Here's another example where I use a ColorPicker to change a NativeScript view's background color: |
| 302 | +
|
| 303 | +`app.gradle` |
| 304 | +
|
| 305 | +[, java] |
| 306 | +---- |
| 307 | +implementation "com.github.skydoves:colorpicker-compose:1.0.0" |
| 308 | +---- |
| 309 | +
|
| 310 | +[, java] |
| 311 | +---- |
| 312 | +package com.example |
| 313 | +
|
| 314 | +import android.content.Context |
| 315 | +import androidx.compose.foundation.layout.fillMaxSize |
| 316 | +import androidx.compose.foundation.layout.fillMaxWidth |
| 317 | +import androidx.compose.foundation.layout.height |
| 318 | +import androidx.compose.foundation.layout.padding |
| 319 | +import androidx.compose.material.MaterialTheme |
| 320 | +import androidx.compose.material.Text |
| 321 | +import androidx.compose.runtime.Composable |
| 322 | +import androidx.compose.runtime.getValue |
| 323 | +import androidx.compose.runtime.mutableStateOf |
| 324 | +import androidx.compose.runtime.setValue |
| 325 | +import androidx.compose.ui.Modifier |
| 326 | +import androidx.compose.ui.graphics.Color |
| 327 | +import androidx.compose.ui.graphics.ImageBitmap |
| 328 | +import androidx.compose.ui.platform.ComposeView |
| 329 | +import androidx.compose.ui.res.imageResource |
| 330 | +import androidx.compose.ui.unit.dp |
| 331 | +import androidx.lifecycle.ViewModel |
| 332 | +import androidx.lifecycle.viewmodel.compose.viewModel |
| 333 | +import com.github.skydoves.colorpicker.compose.ColorEnvelope |
| 334 | +import com.github.skydoves.colorpicker.compose.HsvColorPicker |
| 335 | +import com.github.skydoves.colorpicker.compose.ImageColorPicker |
| 336 | +import com.github.skydoves.colorpicker.compose.rememberColorPickerController |
| 337 | +
|
| 338 | +class ColorPickerCompose { |
| 339 | + fun generateComposeView(view: ComposeView): ComposeView { |
| 340 | + return view.apply { |
| 341 | + setContent { |
| 342 | + val controller = rememberColorPickerController() |
| 343 | + HsvColorPicker( |
| 344 | + modifier = Modifier |
| 345 | + .fillMaxWidth() |
| 346 | + .height(450.dp) |
| 347 | + .padding(10.dp), |
| 348 | + controller = controller, |
| 349 | + onColorChanged = { colorEnvelope: ColorEnvelope -> |
| 350 | + onEvent?.invoke(colorEnvelope.hexCode) |
| 351 | + } |
| 352 | + ) |
| 353 | + } |
| 354 | + } |
| 355 | + } |
| 356 | +
|
| 357 | + fun updateData(value: Map<Any, Any>) {} |
| 358 | +
|
| 359 | + var onEvent: ((String) -> Unit)? = null |
| 360 | +
|
| 361 | +} |
| 362 | +---- |
| 363 | +
|
| 364 | +[, xml] |
| 365 | +---- |
| 366 | +<StackLayout backgroundColor="{{ backgroundColor }}"> |
| 367 | + <Label text="The following view is Jetpack Compose inside NativeScript!" textWrap="true"></Label> |
| 368 | + <StackLayout backgroundColor="lightblue"> |
| 369 | + <jc:JetpackCompose composeEvent="{{ onEvent }}" data="{{ text }}" composeId="jetpackCompose"></sw:JetpackCompose> |
| 370 | + </StackLayout> |
| 371 | + <Label text="This is NativeScript again"></Label> |
| 372 | + <TextView text="{{ backgroundColor }}" textWrap="true"></TextView> |
| 373 | +</StackLayout> |
| 374 | +---- |
| 375 | +
|
| 376 | +video::GcZ156BCGr0[Use Jetpack Compose with NativeScript via @nativescript/jetpack-compose] |
| 377 | +
|
| 378 | +== Final considerations |
| 379 | +
|
| 380 | +Working with Jetpack Compose in NativeScript is very transparent and easy. We look forward in seeing what the community will build with yet another powerful tool in NativeScript's belt! |
0 commit comments