Skip to content

🦋 Butterfly helps you to build adaptive and responsive UIs for Android with Jetpack WindowManager.

License

Notifications You must be signed in to change notification settings

GetStream/butterfly

Repository files navigation

Butterfly

Google
License API Build Status Kotlin Weekly Android Weekly Dokka


🦋 Butterfly helps you to build adaptive and responsive UIs for Android with Jetpack WindowManager.
Also, it supports useful functions for Jetpack Compose and LiveData integration.


Preview

🌗 See dark theme

Dark Theme

Demo Project

The demo project was built with the Stream Chat SDK for Jetpack Compose. It would be helpful to understand the demo project if you check out the links below:

Contribution 💙

Butterfly is maintained by Stream. If you’re interested in adding powerful In-App Messaging to your app, check out the Stream Chat SDK for Android! Also, anyone can contribute to improving code, docs, or something following our Contributing Guideline.

Download

Maven Central

Gradle

Add the codes below to your root build.gradle file (not your module build.gradle file).

allprojects {
    repositories {
        mavenCentral()
    }
}

Next, add the dependency below to your module's build.gradle file.

dependencies {
    implementation "io.getstream:butterfly:1.0.2"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1"
}

Note: Butterfly includes Jetpack WindowManager to compute window internally. So if you're using WindowManager in your project, please make sure your project uses the same version or exclude the dependency to adapt yours.

SNAPSHOT

See how to import the snapshot

Including the SNAPSHOT

Snapshots of the current development version of Butterfly are available, which track the latest versions.

To import snapshot versions on your project, add the code snippet below on your gradle file.

repositories {
   maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
}

Next, add the below dependency to your module's build.gradle file.

dependencies {
    implementation "io.getstream:butterfly:1.0.3-SNAPSHOT"
}

Set up Foldable Emulator

If you don't have foldable devices or emulators, you can set up a foldable emulator environment following the below instruction:

👉 Check out the Set up Foldable Emulator (Surface Duo 2)

Usage

Butterfly uses Jetpack WindowManager, so it would be helpful to understand if you have background knowledge of the WindowManager APIs.

WindowSize

WindowSize represents breakpoints, which are the screen size at which a layout will adapt to best fit content and conform to responsive layout requirements. Butterfly follows three breakpoints by Material Design.

  • WindowSize.Compact: Most phones in portrait mode. (0-599 dp range)
  • WindowSize.Medium: Most foldables and tablets in portrait mode. (600-839 dp range)
  • WindowSize.Expanded: Most tablets in landscape mode. (840+ dp range)

You can get an instance of the WindowSize class with getWindowSize() method on your Activity or Fragment as following below:

val windowSize: WindowSize = getWindowSize()
when (windowSize) {
    is WindowSize.Compact -> // the window size is compact.
    is WindowSize.Medium -> // the window size is medium.
    is WindowSize.Expanded -> // the window size is expanded.
}

GlobalWindowSize

You can customize the pre-defined breakpoints, which used to getWindowSize() with GlobalWindowSize object class as following below:

GlobalWindowSize.compactWindowDpSize = 600
GlobalWindowSize.mediumWindowDpSize = 840

Also, you can fully customize a factory function of the WindowSize class as following below:

GlobalWindowSize.windowSizeFactory = { windowPixelSize ->
    when {
        windowPixelSize.width < 0 -> throw IllegalArgumentException("Can't be negative")
        windowPixelSize.width < 600.dp2Px() -> WindowSize.Compact(windowPixelSize)
        windowPixelSize.width < 840.dp2Px() -> WindowSize.Medium(windowPixelSize)
        else -> WindowSize.Expanded(windowPixelSize)
    }
}

Posture

Fold state: FLAT and HALF-OPENED from Google.

Posture class represents device postures in the flexible display or a hinge between two physical display panels.

  • Posture.TableTop - Device posture is in tabletop mode (half open with the hinge horizontal).
  • Posture.Book - Device posture is in book mode (half open with the hinge vertical).
  • Posture.Normal - Device posture is in normal mode.

You can observe the posture as a Kotlin Flow on you Activity or Fragment as following below:

lifecycleScope.launch(Dispatchers.Main) {
    lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
        postureFlow.collect { posture ->
            when (posture) {
                Posture.Normal -> // posture is Normal
                is Posture.TableTop -> // posture is TableTop
                is Posture.Book -> // posture is Book
            }
        }
        windowLayoutInfo.collect(::onWindowLayoutInfoUpdated)
    }
}

Note: Make sure your project includes Coroutines and androidx.lifecycle:lifecycle-runtime-ktx:2.4.0 dependencies.

WindowLayoutInfo

WindowLayoutInfo contains the list of DisplayFeature-s located within the window. You can observe the WindowLayoutInfo as following below:

lifecycleScope.launch(Dispatchers.Main) {
    lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
        windowLayoutInfo.collect { windowLayoutInfo ->
            // something stuff            
        }
    }
}

You can also get the following information from the WindowLayoutInfo as in the following extensions:

val isSeparating: Boolean = WindowLayoutInfo.isSeparating
val occlusionType: FoldingFeature.OcclusionType = WindowLayoutInfo.occlusionType 
val orientation: FoldingFeature.Orientation = WindowLayoutInfo.orientation
val state: FoldingFeature.State = WindowLayoutInfo.state

FoldingFeature

FoldingFeature that describes a fold in the flexible display or a hinge between two physical display panels. You can utilize the extensions below to check folding states and device postures:

val foldingFeature = windowLayoutInfo.displayFeatures.findFoldingFeature()
val posture = foldingFeature?.toPosture()
val isTableTopPosture = foldingFeature?.isTableTopPosture
val isBookPosture = foldingFeature?.isBookPosture
val isHalfOpened = foldingFeature?.isHalfOpened
val isFlat = foldingFeature?.isFlat
val isVertical = foldingFeature?.isVertical
val isHorizontal = foldingFeature?.isHorizontal

Hinge Size

If your foldable device is separated by a hinge, you can get a hinge size with the extensions below:

val hingePxSize: Int = foldingFeature.hingePxSize
val hingeDpSize: Int = foldingFeature.hingeDpSize
val hingeWidthPxSize: Int = foldingFeature.hingeWidthPxSize
val hingeHeightPxSize: Int = foldingFeature.hingeHeightPxSize
val hingeWidthDpSize: Int = foldingFeature.hingeWidthDpSize
val hingeHeightDpSize: Int = foldingFeature.hingeHeightDpSize

WindowInfoActivity

Butterfly supports WindowInfoActivity, which tracks window configurations and update the WindowLayoutInfo. It has a default windowSize property and onWindowLayoutInfoUpdated abstract method as in the example below:

class MainActivity : WindowInfoActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // windowSize property will be initialized lazily.
        when (windowSize) {
            is WindowSize.Compact -> 
            ...
        }
    }

    override fun onWindowLayoutInfoUpdated(windowLayoutInfo: WindowLayoutInfo) {
        val foldingFeature = windowLayoutInfo.displayFeatures.findFoldingFeature() ?: return
        when (val posture = foldingFeature.toPosture()) {
            Posture.Normal -> Log.d(tag, "[Posture.Normal] ${posture.size}")
            is Posture.TableTop -> Log.d(tag, "[Posture.TableTop] ${posture.size}")
            ...
        }
    }
}

The pre-defined windowSize property will be initialized lazily and the onWindowLayoutInfoUpdated will be updated when the WindowLayoutInfo configuration changed. As the same concept, you can extend WindowInfoFragment for your Fragment.

Butterfly for Jetpack Compose

Maven Central

Butterfly supports Jetpack Compose to build adaptive and responsive UIs. First, add the dependency below to your module's build.gradle file.

dependencies {
    implementation "io.getstream:butterfly-compose:1.0.1"
}

WindowDpSize

WindowDpSize represents breakpoints, which are the screen size at which a layout will adapt to best fit content and conform to responsive layout requirements. Butterfly follows three breakpoints by Material Design.

You can remember an instance of the WindowDpSize class with rememberWindowDpSize() method on your Activity or Fragment as following below:

val windowDpSize: WindowDpSize = rememberWindowDpSize()
when (windowDpSize) {
    is WindowSize.Compact -> MainScreenRegular()
    is WindowSize.Medium -> MainScreenMedium()
    is WindowSize.Expanded -> MainScreenExpanded()
}

Note: Likewise the WindowSize, you can also customize the pre-defined breakpoints, which used to rememberWindowDpSize with the GlobalWindowSize object class.

Posture

You can get a State of Posture to build adaptive and responsive UIs with the postureState extension on your Activity and Fragment as following below:

val postureState: State<Posture> = postureState
when (postureState.value) {
    Posture.Normal -> // posture is Normal
    is Posture.TableTop -> // posture is TableTop
    is Posture.Book -> // posture is Book
}

WindowLayoutInfo

WindowLayoutInfo contains the list of DisplayFeature-s located within the window. You can get the State of the WindowLayoutInfo as following below:

val windowLayoutInfoState: State<WindowLayoutInfo> = windowLayoutInfoState
val foldingFeature = windowLayoutInfoState.value.displayFeatures.findFoldingFeature()
...

CompositionLocal

You can pass instances of the WindowDpSize and Posture down through the Composition implicitly as following below:

CompositionLocalProvider(LocalWindowDpSize provides rememberWindowDpSize()) {
    val windowDpSize = LocalWindowDpSize.current
    ...
}

CompositionLocalProvider(LocalPosture provides postureState.value) {
    val posture = LocalPosture.current
    ...                
}

Hinge Size

If your foldable device is separated by a hinge, you can get a hinge size with the extensions below:

val hingeDp: Dp = FoldingFeature.hingeDp
val hingeDpSize: DpSize = FoldingFeature.hingeDpSize
val hingeWidthDp: Dp = FoldingFeature.hingeWidthDp
val hingeHeightDp: Dp = FoldingFeature.hingeHeightDp

Butterfly for LiveData Integration

Maven Central

Butterfly supports LiveData integration to let observing layout changes as LiveData. First, add the dependency below to your module's build.gradle file.

dependencies {
    implementation "io.getstream:butterfly-livedata:1.0.1"
}

You can observe LiveData of Posture and WindowLayoutInfo on your Activity and Fragment as in the following example below:

postureLiveData().observe(this) { posture ->
    // do something
}

windowLayoutInfoLiveData().observe(this) { windowLayoutInfo ->
    // do something
}

Find this library useful? ❤️

Support it by joining stargazers for this repository. ⭐️
Also, follow Stream on Twitter for our next creations!

License

Copyright 2022 Stream.IO, Inc. All Rights Reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.