Google I/O 2021

tags: google-io, google-io-2021

Keynote

Android 12

  • Biggest Design change ever
  • User Safety and Privacy
  • Bluetooth Permision without Location
  • Widgets
  • Material You Component

Android Jetpack

  • Modern Android Development
  • Android Studio
    • Android Gradle plugin 7.0.0 and new DSL
    • Arctic Fox
  • Kotlin
    • Kotlin Symbol Processing (2x faster than kapt)
  • Jetpack
    • Macrobenchmark
    • DataStore
    • Compose Support
  • Compose
    • Modern UI toolkit for Android
    • Declarative
    • Compatible with View System (ComposeView)
    • 1.0 at July

Building Across Screens

  • Slidingpane Layout
  • Constraint Layout 2.1
  • Wear OS
    • Jetpack Libraries for Wear
      • Tiles
      • Health Services
      • Ongoing Activity
      • Curved Text
      • Watch Faces
      • Complications
      • Remote Interactions
      • Input

What's new in Android

Android 12

  • Color & Motion

    • Richer Palette
    • Color Scheme
    • 2 Natural + 3 Accent colors
  • Widgets

    • Refreshing widgets
  • Launch animations

    • New Splash Screen
    • Activity.getSplashScreen()
  • Notifications

    • Templates redesign
    • Navigation from notification to Activity smoothly
  • Toast

    • Length limitation & number at stack
  • Picture in picture

    • Aplicar efectos sobre View (Blur)
    ​​​​imageView.setRenderEffect(
    ​​​​    RenderEffect.createBlurEffect(
    ​​​​        20.0f, 20.0f, SHADER_TILE_MODE
    ​​​​    )
    ​​​​)
    
    
    • New ripple effect
    • Edge effect
  • Graphics

    • AVIF
  • Video

    • H.264
    • Media capabilities
    ​​​​<media-capabilities>
    ​​​​    <format android:name="HEVC" supported="true"/>
    ​​​​    <format android:name="HDR10" supported="false"/>
    ​​​​    <format android:name="HDR10Plus" supported="false"/>
    ​​​​</media-capabilities>
    ​​​​
    ​​​​<application>
    ​​​​    <property 
    ​​​​        android:name="android.media.PROPERTY_MEDIA_CAPABILITIES"
    ​​​​        android:resource="@xml/media_capabilities"           
    ​​​​    />
    ​​​​
    ​​​​</application>
    
  • Audio

    • Audio-coupled Haptic Playback
    ​​​​if(HapticGenerator.isAvailable()){
    ​​​​    val audioSession = audioTrack.audioSessionId
    ​​​​    generator = HapticGenerator.create(audioSession)
    ​​​​}
    
  • Privacy

    • More control to the user
    • Location
    • Bluetooth Permission
    • Clipboard access
    • Foreground restrictions
  • Expedited Jobs

    ​​​​WorkRequest.Builder.setExpedited()
    ​​​​// Before Android 12 -> Foregorund service
    ​​​​// From Android 12 -> JobInfo.Builder.setExpedited()
    
  • OnReveive ContentListener

    • Before Android 12
      • Drag & Drop
      ​​​​​​​​view.setOnDragListener { view, event ->
      ​​​​​​​​    // react to drag event, get ClipData
      ​​​​​​​​}
      
      • Copy & Paste
      ​​​​​​​​override fun onTextContextMenuItem(id: Int) {
      ​​​​​​​​    // get ClipData from clipboard
      ​​​​​​​​}
      
      • Keyboard Stickets
      ​​​​​​​​InputConnectionCompat.OnCommitContentListener{
      
      ​​​​​​​​}
      
    • Android 12
      • Drag & Drop, Copy & Paste, Keyboard Stickers
      ​​​​​​​​setOnReceiveContentListener(mimeTypes) { view, payload ->
      ​​​​​​​​    // get data from ContentInfo payload
      ​​​​​​​​}
      
  • Performance

    • Measuring Jank and Startup with Macrobenchmark
  • Wear

    • Now it´s de time
  • Android Tools

    • Android Development Tools
    • Design Tools
    • Android Gradle plugin
  • Jetpack: Going Stable

    • CameraX
    • Paging 3
    • Hilt
    • beta DataStore
    • alpha WorkManage 2.7
    • alpha Navigation
    • alpha Macrobenchmark
    • Compose

Top 3 things to know in Modern Android Development

  • Jetpack Libraries

    • Stable
      • Compose (july)
      • Camera X
      • Hilt
      • Paging 3.0
      • Constraint & MotionLayout
      • Security Crypto
      • Fragment
    • Beta
      • DataStore
    • Alpha
      • Macrobenchmark
      • EmojiCompat
      • AppSearch
      • Google Shortcuts
      • Room
      • WorkManager
  • Tooling

    • Database Inspector
    • WorkManager Inspector
    • Layout Inspector
  • Kotlin

What's new in Jetpack

Versioning

  • Alpha
    • Active development
    • APIs may be added, changeg, removed
    • Tested and highly-functional
  • Beta
    • Feature stabilization
    • APIs change onlin in response to critical issues or community feedback

Experimental Annotations (stable)(androidx.annotation:annotation-experimental)

  • Metadata than helps tools and other developers understand your code
    ​​​​@ExperimentalRoomApi
    ​​​​or
    ​​​​@OptIn(ExperimentalRoomAPI::class)
    ​​​​        [UnsageOptInUsageError]
    
    ​​​​builder.setAutocloseTimeout(1000, SECONDS)
    

CameraX (stable)(androidx.camera:camera)

  • Unified API surface for controlling and displaying cameras across a variety of Android OS versions and devices
  • Addressing common feature request and bugs
    • Exposure compensation
    • Access to information about camera state and features
    • Camera2 settings inter-op
  • Support for the latest device and OS features
    • HDR Preview
    • Zoom ratio control
    • Do-Not-Disturb
  • Performance enhancements to provide a better experience
    • Photo capture up to 15% faster
    • Faster initialization (up to 25% on legacy devices)
// Set up the CameraController
val cameraController = LifecycleCameraController(context)
cameraController.bindToLifecycle(lifecycleOwner)

// Attach the CameraController to PreviewView
val previewView = findViewById(R.id.preview_view)
previewView.setController(cameraController)

// Use the CameraController
cameraController.takePicture(...)

AppSearch (alpha) (androidx.appsearch:appsearch)

  • High-performance full-text search for app-local and device-wide use cases
  • Full-featured versus SQL
    • Prefix indexing
    • Relevance ranking
    • Usage scoring
  • Advanced full-text search
    • Fuzzy matching
    • Query expansion
    • Languaje support
  • Includes schemas for common object types based on schemas.org
  • Centralized storage on Android S+ for integrating into device-wide search

DataStore (beta) (androidx.datastore:datastore)

  • Data storage solution tha provides a simple, robust alternative to SharedPreferences
  • Replacement for SharedPreferences
    • Fully asynchronous with Kotlin coroutines / Flow support
  • Multible backing implementations
    • Proto DataStore (strongly typed)
    • Preference DataStore (key-value pairs)
    • Plug in your own
val Context.prefStore: DataStore<Preferences> by preferencesDataStore(
    name = USER_PREFERENCES_NAME,
    produceMigrations = { context ->
        // Since we're migrating from SharedPreferences, 
        // add a migration based on the SharedPreferences name.
        listOf(SharedPreferencesMigration(context, USER_PREFERENCES_NAME))
        
    }
)

Security Crypto (stable) (androidx.security:security-crypto)

  • Safely and easily encrypt Files and SharedPreferences
val prefs: SharedPreferences = EncryptedSharedPreferences.create(
    context,
    "prefs_file_name",
    mainKey,
    prefKeyEncryptionScheme = AES256_SIV,
    prefValueEncryptionScheme = AES256_GCM,
)

// Use the resulting SharedPreferences object as usual
prefs.edit()
    .putBoolean("show_completed", true)
    .apply()

Hilt (stable) (androidx.hilt:hilt)

  • Simplified dependendy injection solution based on Dagger, built for Jetpack
@HiltViewModel
class MyViewModel @Inject constructor(
    val handle: SavedStateHandle,
    val repository: Repository,
) : ViewModel {
    //...
}

@AndroidEntryPoint
class MyFragment : Fragment() {
    val viewModel: MyViewModel by viewModels()
    val navigationGraphScoped: MyViewMode by hiltNavGraphViewModels(R.id.nav_graph)
}

@HiltWorker
class MyWorker @AssistedInject constructor(
    @Assisted context: Context,
    @Assisted params: WorkerParameters,
    repo: Repository,
) : Worker {
    //...
}
  • Testing anotations
@Module
@TestInstallIn(
    components = SingletonComponent::class,
    replaces = RepositoryModule::class,
)

object FakeRepositoryModule {
    @Provides
    fun provideRepo() = FakeRepository()
}

WorkManager (stable) (androidx.work:work)

  • Schedule deferrable, asynchronous tasks thant must be run reliably
  • 2.5+ -> Multiple processes support
  • 2.7 -> Handles Android S foreground restrictions
  • WorkManager Inspector available in Android Studio Arctic Fox

Room (stable) (androidx.room:room)

  • Database persistence layer designed for usability, safety and testability
  • Experimental support for Kotlin Symbol Processing (200% over kapt)
  • Built-in support for enums
  • QueryCallback to simplify tasks like logging after execution
  • ProvidedTypeConverter
@ProvidedTypeConverter
class TimeStampConverter(timeformat: String) {
    @TypeConverter
    fun fromTimestamp(value: Long) {
        //...
    }
}

Room.inMemoryDatabaseBuilder(context, CacheDatabase::class)
    .addTypeConverter(TimeStampConverter(getPreferredTimeFormat()))
    .build

ConstraintLayout (stable) (androidx.constraintlayout:constraintlayout)

  • Flexible, intuitive system for positioning and animating user interfaces
  • version 2.0 with MotionLayout

Fragment (stable) (androidx.fragment:fragment)

  • Abstraction fo segmentin a user interface into reusable components
  • Under-the-hood improvements to increase stability an reduce undocumented behavior
  • ActivityResult integration
  • FragmentOnAttachListener
// Obtain the fragment manager. May be a childFragmentManager,
// if in a fragment, to observe child attachment
val fragmentManager = supportFragmentManager

val listener = FragmentOnAttachListener { fragmentManager, fragment ->
    // Respond to the fragment being attached.
}

fragmentManager.addFragmentOnAttachListener(listener)
  • Fragmework for navigating between destinations within an app, now with multiple backstacks

Google Shortcuts (alpha) (androidx.core:core-google-shortcuts)

  • Dynamic shortcuts for Google Assistant and other Google products
ShortcutInfoCompat siCompat = 
    ShortcutInfoCompat.Builder(context, "id_cappuccino")
        .setShortLabel("Capuccino")
        .setIntent(Intent(context. OrderCappuccion::class.java))
        .addCapability(
            "actions.intent.ORDER_MENU:ITEM",
            "menuItem.name",
            asList("cappuccino")
        )
        .build()
ShortcutManagerCompat.pushDynamicShortcut(context, siCompat)

EmojiCompat (alpha) (androidx.emoji2:emoji2-views)

  • All the emoji, everywere
  • Included by default in AppCompat 1.4.0+

Paging 3.0 (stable) (androidx.paging.paging)

  • Load and display small chunks of data to improve network and system resource consumption
  • First-class support for Kotlin coroutines and Flow
  • Improvements to repository and presentation layers
  • Support for headers, footers, separators and loading state

Macrobenchmark (alpha) (androidx.benchmark.macro:macro)

  • Accurately measure app startup and scrolling performance, both locally and in CI
  • View performance profiling results directly in Android Studio
  • Check for regressions in CI before they hit users

Compose integrations

  • Hilt, Paging, Navigation, Activity, ViewModel, ConstraintLayout

Form factors

  • androidx.window
  • androidx.slidingpanlayout
  • androidx.wear

State of Kotlin on Android

Momentum

  • Usage
    • 80% Top 1000 apps
    • 60% professional developers
    • Kotlin on services
  • Benefits
    • Expressivenes
    • Safety
    • Interroperability
    • Structured Concurrency
  • Null Safety
    • 10% reduction of user crashed

  • Kotlin Google Developer Experts (50)s

Tools updates

  • Gradle DSL with Android Gradle Plugin 7.0.0 (alpha)
  • Kotlin Symbol Processing (beta)
    • First-class Symbol Processing in Kotlin
    • All Kotlin Symbols
    • Faster Builds (2x)
    • Rich APIs
  • Rewrite Compiler (from Kotlin 1.5)

Libraries APIs

  • Android KTX
    • Play Core
    • Maps & Places
    • Rich APIs
  • New libraries with Kotlin
  • Compose
  • Kotlin gRPC (1.0 December 2020)
  • Kotlin Protobuf bindings (1.0 May 2021)
  • Coroutines
    • Flows
    • StateFlow
    • SharedFlow
    • LiveData to UIs, but StateFlows
    • Coroutines Debugger
  • Deprecation
    • Synthetics for Views -> Migrate to ViewBinding
    • Parcelize: change plugin "kotlin-parcelize" and import import kotlinx.parcelize.Parcelize

Modern languaje features

  • Operator overloaging
  • Extension functions
  • Named parameters
  • Coroutines
  • Data classes

What's new in Android development tools

Roadmap

  • Android Studio 4.1
    • Database Inspector
    • Tensorflow lite models
    • Native memory profiler
  • Android Studio 4.2
    • Project upgrade assistant
    • Safe Args support
    • Apply changes update
  • Versioning Schema
    • Artic Fox (2020.3.1)
      • 2020 -> Year of Intellij Version
      • 3 -> Intellij Major Version
      • Studio Major Version

Design

  • Jetpack Compose
    • Live literals
    • Preview View
    • Constraint Preview
    • Interactive preview
  • Animated drawables
  • Motion editor update
  • Layout validation & inspector
  • Accesible scanner

Devices

  • Different Screen Sizes
  • Tablets/Foldables
  • WearOS
  • Android TV
  • Automotive OS
  • General
    • Device manager
    • Wireless ADB

Productivity

  • Test retention
    • Android emulator snapshots wich can capture test state failures
    • Instrumentation test
  • Profiling
  • WorkManager Inspector
  • IDE Refactoring tool
    • Migrate to Non-Transitive R Classes

Design Tools News

Design Tools

  • Layout Editor
    • Design surface improvements
    • Support for ConstraintLayout 2.1
    • Accessibility Scanner
  • Layout Validation
    • Performance improvements
    • Support for Locales
    • Wear & Tablet
  • Layout Inspector
    • Stability and performance improvements
    • Compose support
  • Animated Vector Drawable
    • Preview Animated Vector Drawable

ConstraintLayout 2.0

  • VirtualLayout: Flow
  • Performance improvements
  • MotionLayout
    • Build in ConstraintLayout 2.0
    • Coordinate easily animations of multiple views
    • Seekable, reversible animations
  • Motion Editor
    • Android Studio 4.0+
    • Build, edit and preview animations
    • Convert from ConstraintLayout in 4.1 & Artic Fox

ConstraintLayout 2.1

  • Constraints

  • Flow improvements

  • Carousel

    • VirtualLayout to create Carousels
    • Easily build complex effects
  • Rotation support

    • Rotation paths
    • Rotation gestures
  • Motion Effect

    • Motion Helper.
    • Apply effectos on a set of views depending on their movement
  • ViewTransitions

    • Specify reusable single view transitions
    • Trigger them as neede or use them with MotionEffect
  • Motion Label

    • Motion helper
    • Animate labels easily
    • Live resize, texture
  • Foldable

    • Inject fold position in your layout using a ReactiveGuide helper
    • With MotionLayout, updates to the fold can be animated automatically
    • MotionLayout can alse be used to model different device states (open, fold, etc) and animate the transitions between them
    ​​​​<androidx.constraintlayout.widget.ReactiveGuide
    ​​​​    android:id="@+id/fold"
    ​​​​    android:layout_width="wrap_content"
    ​​​​    android:layout_height="wrap_content"
    ​​​​    android:orientation="horizontal"
    ​​​​    app:reactiveGuide_animateChange="true"
    ​​​​    app:reactiveGuide_valueID="@id/fold" 
    
    ​​​​if(foldFeature.isSeparating){
    ​​​​    val foldPosition = //get position of the fold
    ​​​​    ConstraintLayout.getSharedValues().fireNewValue(R.id.fold, fold)
    ​​​​    myMotionLayout.transitionToState(R.id.halfopen)
    ​​​​} else {
    ​​​​    ConstraintLayout.getSharedValues().fireNewValue(R.id.fold, 0)
    ​​​​    myMotionLayout.transitionToState(R.id.open)
    ​​​​}
    

ConstraintLayout in Compose

  • ConstraintLayout-Compose 1.0
  • Same ConstraintLayout engine
  • Relative positioning
  • DSL/JSON
  • MotionLayout

Compose Tools

  • Compose Preview
    ​​​​@Preview
    ​​​​@Composable
    ​​​​fun PostPreview() {
    ​​​​    ThemedPreview {
    ​​​​        GenericProvider()
    ​​​​        PostCartTop(post = post1)
    ​​​​    }
    ​​​​}
    
  • Live Literals
  • Sample Data
    ​​​​class PostProvider: CollectionPreviewPArameterProvider<Post>(
    ​​​​    listOf(
    ​​​​        post1,
    ​​​​        post2,
    ​​​​        post3,
    ​​​​        post4,
    ​​​​        post5,
    ​​​​    )
    ​​​​)
    ​​​​
    ​​​​@Preview
    ​​​​@Composable
    ​​​​fun PostPreview(@PreviewParameter(provider = PostProvider::class) previewPost: Post) {
    ​​​​    ThemedPreview {
    ​​​​        GenericProvider()
    ​​​​        PostCartTop(post = previewPost)
    ​​​​    }
    ​​​​}
    
  • Interactive Preview
  • Anitation Preview

Macrobenchmark: Measuring Jank and Startup

Measuring performance is important

  • Metrics from the field are not enough
  • Performance testing is hard
    • Want local measurement
    • Existing Benchmark library is limited
    • Stable measurement is not easy

Introducing Macrobenchmark (androidx.benchmark.macro)

  • Measures real performance on real device

  • Captures startup and jank metrics and traces

  • Enables custom scenarios

    • Metrics
      • StartupTimingMetric: App launch time
      • FrameTimingMetric: Cost of producing each frame
    • CompilationMode
      • None: Worst case
      • SpeedProfile: Common case
      • Speed: Best case
    • StartupMode
      • Hot: Activity.onResume
      • Warm: Activity.onStart
      • Cold: Process Init
@get:Rule
val benchmarkRule = MacrobenchmarkRule()

@Test
fun startup() = benchmarkRule.measureRepeated(
    packageName = "mypackage.myapp",
    metrics = listOf(StartupTimingMetric()),
    compilationMode = CompilationMode.SpeedProfile(),
    startupMode = StartupMode.COLD,
    iterations = 5,
    
) {
    pressHome()
    startActivityAndWait(Intent().apply {
        action = "mypackage.myapp.myaction"
    })
}

How does it work

  • Setup -> Test -> Process -> Present Results
    • Metrics: results.json
    • Traces: iter0.trace, iter1.trace, etc
    • Studio Summary

Benchmarking With Studio

  • At Android Studio Artic Fox
    • activityStart
    • resource loading
    • view inflation
    • loading icons

Tracing (androix.tracing.trace)

import androidx.tracing.trace;

fun loadIcon() = trace("LoadingIcon"){
    // ...
}

What's new in Android testing tools

How we're making testing more

  • Reliable
    • All unit and instrumentation test run throgh a unified Android Gradle test runner, learning to more consistent results
  • Scalable
    • Parallel device testing streaming result to all-new Test Matrix. Integration of UTP enables functionality for more scalable testing, like Gradle Managed Devices
  • Productive
    • Rich test results enable by UTP, such as Emulator Snapshots for test failures, streamed back to the Test Matrix

Better experience

  • Use Android Gradle Plugin to execute Android Test since Artic Fox
  • Enable Run Android Instrumented Tests using Gradle
  • Under the hood, is delegating to Android Test Runner

What is UTP

  • A Unified Test Platform
    • Extensible platform that abstracts the testing environment from feature development.
  • Modular vía plugins
    • Functionality is added to test execution via modular plugins, allowing features to be developed and updated independently
  • Shorter release cycles
    • Modular development than only needs to be implemented one way for all testing environment leads to more features, sooner

Gradle Manageg Devices

  • Define virtual devices and device groups using Gradle DSL
  • Device lifecycles fully managed by Android Gradle plugin (AGP)
  • AGP downloads any required images and SDK components
  • Caches test results and utilizes emulator snapshots for faster continuous testing
android {
    testOptions {
        devices {
            pixel2api29(com.android.build.api.dsl.ManagedVirtualDevice) {
                device = "Pixel 2"
                apiLevel = 29
                systemImageSource = "google"
                abi = "x86"
            }
            nexus9api30(com.android.build.api.dsl.ManagedVirtualDevice) {
                device = "Nexus 9"
                apiLevel = 30
                systemImageSource = "google"
                abi = "x86"
            }
        }
        deviceGroups {
            phoneAndTablet {
                targetDevices.addAll(devices.pixel2api29, devices.nexus9api30)
            }
        }
    }
}
//Execute at expecific device
gradlew -Pandroid.experimental.androidTest.useUnifiedTestPlatform=true
[device-name][BuildVariant]AndroidTest

gradlew -Pandroid.experimental.androidTest.useUnifiedTestPlatform=true
pixel2api29DebugAndroidTest

//Execute in group
gradlew -Pandroid.experimental.androidTest.useUnifiedTestPlatform=true
[group-name]Group[BuildVariant]AndroidTest

gradlew -Pandroid.experimental.androidTest.useUnifiedTestPlatform=true
phoneAndTabletGroupDebugAndroidTest
  • Enable snapshots for Intrumentation Test
android {
    testOptions {
        emulatorSnapshots {
            enableForTestFailures = true
            maxSnapshotsForTestFailures = 2
        }
    }
}

What's new in Jetpack Compose

Compose 1.0 at July

Declarative UI Toolkit

Actual Visual System

  • Complex to mantain UI state
  • XML Layout (state)
    • TextView (state)
    • Button (state)
    • LinearLayout (state)
  • Activity/Fragment (state)
    • findViewById()
    • label.setText()
    • button.setVisibility()
    • linearContainer.addView()

Declarative System

State1 -> UI1
State2 -> UI2

@Composable
fun ConversationScreen()&nbsp;{
    val viewModel: ConversationViewModel = viewModel()
    val messages by viewModel.messages.observeAsState()
    MessagesList(messages)
}

@Composable
fun MessageList(messages: List<String>) {
    Column {
        if (messages.size == 0) {
            Text("No messages")
        } else {
            messages.forEach { message ->
                Text(text = message)
            }
        }
        
    }
}
// Manage state internally
@Composable
fun MessageList(messages: List<String>) {
    Column {
        var selectAll by remember { mutableStateOf(false) }
        Checkbox(
            checked = selectAll,
            onCheckChange = { checked ->
                selectAll = checked
            }
        )
    }
}

// or externally 

@Composable
fun MessageList(
    messages: List<String>,
    selectAll: Boolean,
    onSelectAll: (Boolean) -> Unit,
) {
    Column {
        Checkbox(
            checked = selectAll,
            onCheckChange = onSelectAll
        )
    }
}
  • You completely descrube your UI for a given state
  • The framework updates your UI when the state changes

Compose ViewModel with State Architecture

UI Toolkit

  • Button
  • Text
  • Card
  • FloatingActionButton
  • TopAppBar
  • MaterialTheme: colors, typography, shapes
  • Row -> Horizontal LinearLayout
  • Column -> Vertical LinearLayout
  • Box -> FrameLayout
  • ConstraintLayout
ConstraintLayout&nbsp;{
    OutlinerAvatar(modifier = Modifier.constrainAs(avatar){
        centerHorizontallyTo(parent)
        centerAround(image.bottom)
    })
}
  • Layout
Layout(
    content = content,
    modifier = modifier,
) { measurables, constrains ->
    
    // Do some calculations
    
    layout(width, height) {
        placeables.forEach { placeable ->
            placeable.place(x, y)
        }
    }
}

Animation System

val radius by animateDpAsState(
    if(selected) 28.dp else 0.dp
)

Surface(
    shape = RoundedCornerShape(
        topStart = radius
    )
) {
    ...
}

Accesibillity

@Composable
fun ShareButton(onClick: () -> Unit) {
    IconButton(onClick = onClick) {
        Icon(
            imageVector = Icons.Filled.Share,
            contentDescription = stringResource(R.string.label_share) 
        )
    }
}

Testing

@Test
fun circleAnimation_midAnimation_screenshot() {
    // Test setup

    // Advance clock
    composeTestRule.mainClock.advbanceTimeBy(600)
    
    // Assertions
}

Made over Kotlin

@Composable
fun MessageList(
    messages: List<Message>,
    onClick: () -> Unit,
) {
    val selectAll by remember { mutableStateOf(false) }
    LazyList {
        items(messages) { message ->
            MessageItem(onClick)
        }
    }
}
  • Coroutines
Box(
    modifier = Modifier.pointerInput {
        coroutineScope{
            while (true) {
                val offset = awaitPointerEventScope&nbsp;{
                    awaitFirstDown().position
                }
                launch {
                    animatedOffset.animateTo(offset)
                }
            }
        }
    }
)  {

}

Owl App Example

@Composable
fun YellowTheme(
    content: @Composable () -> Unit
){
    val colors = if(isSystemInDarkTheme()){
        YellowThemeDark
    } else {
        YellowThemeLight
    }
    MaterialTheme(colors, content)
}

@Composable
fun TopicsScreen(){
    YellowTheme&nbsp;{
        Scaffold(
            backgroundColor = MaterialTheme.colors.surface
        ) {
            TopicsList()
        }
    }
}

@Composable
fun TopicsList(topics: List<Topic>) {
    LazyColumn(modifier = modifier) {
        items(topics) { topic ->
            TopicChip(topic)
        }
    }
}

@Composable
fun TopicChip(topic: Topic, selected: Boolean) {
    val radius by abunateDpAsState(
        if (selected) 20.dp else 0.dp
    ) 
    Card(
        shape = RoundedCornerShape(
            topStart = radius
        )
    ){
        Row {
            Box{
                Image(topic.image)
                if(selected){
                    Icon(Icons.Filled.Done)
                }
            }
            Text(text = topic.name)
        }
    }
    
}

Build to Interop

Compose <-> View

Using Jetpack libraries in Compose

Jetpack Libraries

  • Room
  • Paging
  • Coroutines/Flow
  • ViewModel
  • Hilt

HomeViewModel implementation

data class HomeUiState(
    val plantCollections: List<Collection<Plant>> = emptyList(),
    val loading: Boolean = false,
    val refreshError: Boolean = false,
)

@HiltViewModel
class HomeViewModel @Inject constructor(
    private val plantsRepository: PlantsRepository
) : ViewModel() {

    private val _uiState = MutableStateFlow(HomeUiState(loading = true))
    val uiState: StateFlow<HomeUiState> = _uiState
    
    val pagedPlants: Flow<PagingData<Plant>> = plantsRepository.plants
    
    init {
        viewModelScope.launch {
            val collections = plantsRepository.getCollections()
            _uiState.value = HomeUiState(plantCollections = collections)
        }
    }

}

Home Screen Implementation

@AndroidEntryPoint
class HomeFragment : Fragment(){
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ) = ComposeView(requireContext()).apply {
        setContent {
            val homeViewModel: HomeViewModel = viewModel()
            val uiState by hombeViewModel.uiState.collectAsState()
            val plantList = homeViewModel.pagedPlants
            
            // HomeScreen is now totally independent of the HomeViewModel
            HomeScreen(uiState, plantList, onPlantClick = {
                // TODO: Navigate to Plant Detail screen
            })
        }
    }
}




@Composable
fun HomeScreen(
    uiState: HomeUiState,
    plants: Flow<PagedData<Plant>>,
    onPlantClick: (Plant) -> Unit,
) {
    
    if(uiState.loading) {
    
    } else {
        SearchPlants()
        CollectionsCarousel(
            carouselState = uiState.carouselState,
            onPlantClick = onPlantClick
        )
        PlantList(plants)
    }
}


@Composable
fun PlantList(plants: Flow<PagingData<Plant>>) {
    val pagedPlantItem
        if(pagedPlantItems.loadState.refresh == LoadState.Loading) {
            item { LoadingIndicator() }
        }
    
        itemsIndexed(pagedPlantItems) { index, plant ->
            if(plant != null){
                PlantItem(plant)
            } else {
                PlantPlaceholder()
            }
        }
        
        if(pagedPlantItems.loadState.append == LoadingState.Loading)&nbsp;{
            item { LoadingIndicator() }
        }
    }
}

How to reuse a Composable

@Composable
fun CollectionsCarousel(
    //State in,
    //Event out
    carouselState: CollectionsCarouselState,
    onPlantClick: (Plant) -> Unit,
) {
    //...
}


data class PlantCollection(
    val name: String,
    @IdRes val asset: Int,
    val plants: List<Plant>
)

class CollectionsCarouselState(
    private val collections: List<PlantCollection>
){
    var selectedIndex: Int? by mutableStateOf(null)
        privateSet
        
    val isExpanded: Boolean
        get() = selectedIndex != null
        
    var plants by mutableStateOf(emptyList<Plant>())
        private set
        
    fun onCollectionClick(index: Int) {
        if(index >= collections.size || index < 0) return
        if(index == selectedIndex) {
            selectedIndex = null
        } else {
            plants = collections[index].plants
            selectedIndex = index
        }
    }
    // ...
}

State Holders

  • ViewModel type: High level screen composables - first composable used in an Activity, Fragment or destination of Navigation graph

    • For example: HomeScreen or LoginScreen
  • Regular class: Reusable composables that could be potentially used multiple times on the same screen

    • For example: Carousel or ExpandableCard

ViewModel

  • Benefits as state holder of a screen level composable
    • Survives configuration changes
    • Data can survive process death via SavedStateHandle
    • Better place for state whose lifecycle matches the screen
    • Built-in coroutine scope: viewModelScope
  • Migration steps

    • Navigation with Fragments -> Navigation with Fragments with Compose code -> Navigation Compose
    • Using Fragments with View and XML -> Using Fragments with Compose code -> Using Navigation Compose
  • Navigation component: A generic runtime with:

    • Kotlin DSL
    • Automatic scoping of Lifecycle, ViewModels and Saved State
    • Deep linking
    • Returning results
    • System back button integration
setContent {
    val navController = rememberNavController()
    Scaffold(
        bottomBar = { /* Can use the NavController */ }
    ) { innerPadding ->
        NavHost(
            navController,
            startDestination = "home",
            modifier = Modifier.padding(innerPadding)
        ) {
            // ...
        }
    }
}
NavHost(navController, startDestination = "home") {
    composable(route = "home") {
        val homeViewModel: HomeViewModel = hiltNavGraphViewModel()
        val uiState by hombeViewModel.uiState.collectAsState()
        val plantList = homeViewModel.pagedPlants

        // HomeScreen is now totally independent of the HomeViewModel
        HomeScreen(uiState, plantList, onPlantClick = { plant ->
            navController.navigate("plant/${plant.id}")
        })
    }
    composable(
        route = "plant/{id}",
        arguments = listOf(navArgument("id") { type = NavType.IntType }),
    ) { backstackEntry ->
        val plantViewModel: PlantViewModel = hiltNavGraphViewModel()
        val plant by plantViewModel.plantDetails.collectAsState()
        PlantDetailScreen(plant)
    }
}
@HiltViewModel
class PlantViewModel @Inject constructor(
    plantsRepository: PlantsRepository,
    savedStateHandle: SavedStateHandle,
) : ViewModel() {

    val plantDetails: Flow<Plant> = plantsRepository.getPlantDetails(
        savedStateHandle.get<In>("id")
    )
    
}