Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Brick by Brick: Building Open Source libraries

Brick by Brick: Building Open Source libraries

So you have an idea, and you want to publish a library for it? But where do you start? Doing Open Source is a fine art which requires skill you don’t easily learn on schoolbooks. Creating a new library is like building a new house that people want to live in: you need to start with a great foundation, build your inner walls and then add all the niceties that make your house the best place to live in.

Join us as we share our journey building Open Source Android libraries: we’ll start from tools to help you organize your code, we’ll learn how to publish your libraries publicly and how to effectively maintain them. Throughout this journey, we’ll share our experience maintaining popular Android libraries such as Detekt, Chucker and AppIntro.

A house won’t be a home without someone living inside it, though. As your library won’t be a popular library without a strong community around it. So we’ll have the opportunity to share our insights on building strong communities around Open Source, getting developers together, finding new contributors and dealing with maintainer burnout.

Curious to know how to build a shiny new library brick by brick? Then make sure to don’t miss out this talk, and we can’t wait to see what you all will be building!

Nicola Corti

November 26, 2023
Tweet

More Decks by Nicola Corti

Other Decks in Technology

Transcript

  1. PAOLO ROTOLO
    @paolorotolo
    NICOLA CORTI
    @cortinico

    View full-size slide

  2. • Android Dev @ Nextome
    • AppIntro Author, Piano Player
    • twitter.com/paolorotolo
    • github.com/paolorotolo
    NICOLA CORTI
    PAOLO ROTOLO
    • Kotlin GDE
    • twitter.com/cortinico
    • github.com/cortinico

    View full-size slide

  3. detekt/detekt
    ChuckerTeam/Chucker
    AppIntro/AppIntro

    View full-size slide

  4. Learn to say no

    View full-size slide

  5. Learn to say no
    • Build elegant APIs
    • Do not break APIs
    • Future proof your APIs

    View full-size slide

  6. Learn to say no
    • Build elegant APIs
    • Do not break APIs
    • Future proof your APIs

    View full-size slide

  7. Build elegant APIs
    list.first()

    list.firstOrNull()

    list.single()

    list.singleOrNull()

    View full-size slide

  8. class DefaultIntro : AppIntro() {

    override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState)

    addSlide(AppIntroFragment.createInstance(

    title = "Title 1",

    description = "Desc 1",

    imageDrawable = R.drawable.ic_slide1

    ))

    addSlide(AppIntroFragment.createInstance(

    title = “Title 2",

    description = "Desc 2",

    imageDrawable = R.drawable.ic_slide2

    ))

    setTransformer(AppIntroPageTransformerType.Fade)

    isWizardMode = true

    isColorTransitionsEnabled = true

    Build elegant APIs

    View full-size slide

  9. AppIntroScreen {

    slides {

    slide {

    title = "Title 1"

    description = "Desc 1"

    imageDrawable = R.drawable.image1

    backgroundColor = R.color.orange

    }

    slide {

    title = "Title 2"

    description = "Desc 2"

    imageDrawable = R.drawable.image2

    backgroundColor = R.color.blue

    }

    }

    settings {

    transformer = transformer {

    AppIntroPageTransformerType.

    }

    isWizardMode = true

    Fade
    Build elegant APIs

    View full-size slide

  10. /**

    * Sealed class to represent all the possible Page Transformers

    * offered by AppIntro.

    */


    sealed class AppIntroPageTransformerType {

    object Flow : AppIntroPageTransformerType()

    object Depth : AppIntroPageTransformerType()

    object Zoom : AppIntroPageTransformerType()

    object SlideOver : AppIntroPageTransformerType()

    object : AppIntroPageTransformerType()

    class Parallax(

    val titleParallaxFactor: Double = 1.0,

    val imageParallaxFactor: Double = -1.0,

    val descriptionParallaxFactor: Double = 2.0,

    @IdRes val titleViewId: Int = R.id.title,

    @IdRes val imageViewId: Int = R.id.image,

    @IdRes val descriptionViewId: Int = R.id.description

    ) : AppIntroPageTransformerType()

    }
    Fade
    Build elegant APIs

    View full-size slide

  11. when (transformType) {

    AppIntroPageTransformerType.Flow
    ->
    {

    page.rotationY = position * FLOW_ROTATION_ANGLE

    }

    AppIntroPageTransformerType.SlideOver
    ->
    transformSlideOver(position, page)

    AppIntroPageTransformerType.Depth
    -
    >
    transformDepth(position, page)

    AppIntroPageTransformerType.Zoom
    ->
    transformZoom(position, page)

    AppIntroPageTransformerType.Fade
    ->
    transformFade(position, page)

    is AppIntroPageTransformerType.Parallax
    ->
    {

    titlePF = transformType.titleParallaxFactor

    imagePF = transformType.imageParallaxFactor

    descriptionPF = transformType.descriptionParallaxFactor

    transformParallax(

    position,

    page,

    transformType.titleViewId,

    transformType.imageViewId,

    transformType.descriptionViewId

    )

    }

    }

    Build elegant APIs

    View full-size slide

  12. class DefaultIntro : AppIntro() {

    override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState)

    addSlide(

    AppIntroFragment.createInstance(

    title = "Welcome!",

    description = "This is a demo of the AppIntro library",

    imageDrawable = R.drawable.ic_slide1

    )

    )
    Build elegant APIs

    View full-size slide

  13. class DefaultIntro : AppIntro() {

    override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState)

    addSlide(

    AppIntroFragment.createInstance(

    title = "Welcome!",

    description = "This is a demo of the AppIntro library",

    imageDrawable = R.drawable.ic_slide1

    )

    )
    Build elegant APIs

    View full-size slide

  14. addSlide(

    AppIntroFragment.createInstance(

    title = "Welcome!",

    description = "This is a demo of the AppIntro library",

    imageDrawable = R.drawable.ic_slide1

    )

    )
    Build elegant APIs

    View full-size slide

  15. addSlide(

    AppIntroCustomLayoutFragment.newInstance(

    R.layout.intro_custom_layout1

    )

    )
    Build elegant APIs

    View full-size slide

  16. Learn to say no
    • Build elegant APIs
    • Do not break APIs
    • Future proof your APIs

    View full-size slide

  17. abstract class AppIntro : AppIntroBase() {

    override val layoutId = R.layout.appintro_intro_layout

    /**

    * Override viewpager bar color

    * @param color your color resource

    */


    fun setBarColor(@ColorInt color: Int) {

    val bottomBar = findViewById(R.id.bottom)

    bottomBar.setBackgroundColor(color)

    }

    Do not break APIs

    View full-size slide

  18. public abstract class AppIntro : AppIntroBase() {

    override val layoutId = R.layout.appintro_intro_layout

    /**

    * Override viewpager bar color

    * @param color your color resource

    */


    public fun setBarColor(@ColorInt color: Int) {

    val bottomBar = findViewById(R.id.bottom)

    bottomBar.setBackgroundColor(color)

    }

    Do not break APIs
    Kotlin Explicit API mode

    View full-size slide

  19. public abstract class AppIntro : AppIntroBase() {

    override val layoutId: Int = R.layout.appintro_intro_layout

    /**

    * Override viewpager bar color

    * @param color your color resource

    */


    public fun setBarColor(@ColorInt color: Int) {

    val bottomBar = findViewById(R.id.bottom)

    bottomBar.setBackgroundColor(color)

    }

    Do not break APIs
    Kotlin Explicit API mode

    View full-size slide

  20. fun goToSlide(

    number: Int

    ) {

    TODO()

    }

    Do not break APIs

    View full-size slide

  21. fun goToSlide(

    number: Int,

    animate: Boolean,

    ) {

    TODO()

    }

    Do not break APIs

    View full-size slide

  22. fun goToSlide(

    number: Int,

    animate: Boolean = false,

    ) {

    TODO()

    }

    Do not break APIs

    View full-size slide

  23. fun goToSlide(

    number: Int,

    animate: Boolean = false,

    ) {

    TODO()

    }

    void goToSlide(Integer number, boolean animate);
    Do not break APIs

    View full-size slide

  24. fun goToSlide(

    number: Int,

    animate: Boolean = false,

    ) {

    TODO()

    }

    void goToSlide(Integer number, boolean animate);
    @JvmOverloads

    void goToSlide(Integer number);

    Do not break APIs

    View full-size slide

  25. Don’t use Data Classes
    in public APIs
    Do not break APIs

    View full-size slide

  26. bit.ly/brokenapi
    Do not break APIs

    View full-size slide

  27. @Deprecated(

    level = DeprecationLevel.WARNING

    )

    fun newInstance(sliderPage: SliderPage) = createInstance(sliderPage)

    replaceWith = ReplaceWith("createInstance(sliderPage)"),

    message = "`newInstance` is deprecated to support color resources instead of color int " +

    "for configuration changes and dark theme support",
    Do not break APIs

    View full-size slide

  28. @Deprecated(

    level = DeprecationLevel.WARNING

    )

    fun newInstance(sliderPage: SliderPage) = createInstance(sliderPage)

    replaceWith = ReplaceWith("createInstance(sliderPage)"),

    message = "`newInstance` is deprecated to support color resources instead of color int " +

    "for configuration changes and dark theme support",
    Do not break APIs

    View full-size slide

  29. @Deprecated(

    level = DeprecationLevel.WARNING

    )

    fun newInstance(sliderPage: SliderPage) = createInstance(sliderPage)

    replaceWith = ReplaceWith("createInstance(sliderPage)"),

    message = "`newInstance` is deprecated to support color resources instead of color int " +

    "for configuration changes and dark theme support",
    Do not break APIs

    View full-size slide

  30. @Deprecated(

    level = DeprecationLevel.WARNING

    )

    fun newInstance(sliderPage: SliderPage) = createInstance(sliderPage)

    replaceWith = ReplaceWith("createInstance(sliderPage)"),

    message = "`newInstance` is deprecated to support color resources instead of color int " +

    "for configuration changes and dark theme support",
    Do not break APIs

    View full-size slide

  31. DeprecationLevel.WARNING

    DeprecationLevel.ERROR

    DeprecationLevel.HIDDEN

    Do not break APIs

    View full-size slide





  32. SKIP

    string>

    NEXT

    string>

    BACK

    string>

    DONE

    string>


    resources>

    Don’t forget about Resources
    Do not break APIs

    View full-size slide





  33. SKIP

    string>

    NEXT

    string>

    BACK

    string>

    DONE

    string>


    resources>

    Do not break APIs
    Don’t forget about Resources

    View full-size slide

  34. Learn to say no
    • Build elegant APIs
    • Do not break APIs
    • Future proof your APIs

    View full-size slide

  35. Retrofit

    Room

    Joda Time

    Timber

    Moshi

    Paging
    -
    >


    -
    >


    -
    >


    -
    >


    -
    >


    -
    >
    Ktor

    SQLDelight

    DataTime

    Kermit

    kotlinx-serialization

    Cash App’s Paging
    Future proof your APIs

    View full-size slide

  36. Avoid asking for
    Context in public
    APIs
    Future proof your APIs

    View full-size slide

  37. let myLib = MyLibrary.getInstance()
    myLib.doSomething()
    val myLib = MyLibrary.getInstance(

    applicationContext)

    myLib.doSomething()

    Future proof your APIs

    View full-size slide

  38. let myLib = MyLibrary.getInstance()
    myLib.doSomething()
    val myLib = MyLibrary.getInstance()

    myLib.doSomething()

    Future proof your APIs

    View full-size slide

  39. let myLib = MyLibrary.getInstance()
    myLib.doSomething()
    val myLib = MyLibrary.getInstance()

    myLib.doSomething()

    bit.ly/AvoidAskingForContext
    Future proof your APIs

    View full-size slide

  40. JitPack GitHub
    Packages
    Maven
    Central

    View full-size slide

  41. JitPack
    • Easy to use
    • Free for OSS
    • Good for starting
    • No support for artifacts
    signing
    • No support for Kotlin
    Multiplatform (yet)

    View full-size slide

  42. JitPack
    • Easy to use
    • Free for OSS
    • Good for starting
    • No support for artifacts
    signing
    • No support for Kotlin
    Multiplatform (yet)
    jitpack/jitpack.io #3853

    View full-size slide

  43. GitHub
    Packages

    View full-size slide

  44. GitHub Packages
    • Easy integration with Actions
    • Code and artifacts on the
    same repo
    • Supports Kotlin Multiplatform
    • Users are required to login
    with a token

    View full-size slide

  45. dependencyResolutionManagement {

    repositories {

    google()

    mavenCentral()

    maven {

    name = "GitHubPackages"

    url = uri("https:
    //
    maven.pkg.github.com/Nextome/KmmBeacons")

    credentials {

    username = "gh_username"

    password = "gh_token"

    }

    }

    }

    }

    settings.gradle (user)

    View full-size slide

  46. dependencyResolutionManagement {

    repositories {

    google()

    mavenCentral()

    maven {

    name = "GitHubPackages"

    url = uri("https:
    //
    maven.pkg.github.com/Nextome/KmmBeacons")

    credentials {

    username = "gh_username"

    password = "gh_token"

    }

    }

    }

    }

    settings.gradle (user)

    View full-size slide

  47. Maven Central

    View full-size slide

  48. Maven Central
    • Popular, included by default in
    Android projects
    • Open Source Software Repository
    Hosting (OSSRH) for free
    • More secure
    • Requires more effort and time to
    set up

    View full-size slide

  49. Don’t reinvent the wheel

    View full-size slide

  50. github.com/cortinico/kotlin-android-template
    Don’t reinvent the wheel

    View full-size slide

  51. Gradle Source Dependencies
    Bonus

    View full-size slide

  52. sourceControl {

    gitRepository(URI("https:
    //
    github.com/AppIntro/AppIntro.git")) {

    producesModule("com.github.AppIntro:AppIntro")

    rootDir = "appintro"

    }

    }

    settings.gradle
    Gradle Source Dependencies

    View full-size slide

  53. sourceControl {

    gitRepository(URI("https:
    //
    github.com/AppIntro/AppIntro.git")) {

    producesModule("com.github.AppIntro:AppIntro")

    rootDir = "appintro"

    }

    }

    settings.gradle
    implementation("com.github.AppIntro:AppIntro:SNAPSHOT") {

    version {

    branch = "main"

    }

    }

    build.gradle
    Gradle Source Dependencies

    View full-size slide

  54. sourceControl {

    gitRepository(URI("https:
    /
    producesModule("com.github.AppIntro:AppIntro")

    rootDir = "appintro"

    }

    }

    settings.gradle
    implementation("com.github.AppIntro:AppIntro:SNAPSHOT") {

    version {

    branch = "main"

    }

    }

    build.gradle
    Gradle Source Dependencies

    View full-size slide

  55. We are getting there. help voting
    bit.ly/gradlesourcedeps
    Gradle Source Dependencies

    View full-size slide

  56. Tell the world

    View full-size slide

  57. Setting the right
    expectations

    View full-size slide

  58. • Follow a schema
    • Feature-based
    • Release-train
    • ROADMAP.md
    fi
    le
    Predictable Releases

    View full-size slide

  59. Conventional Commits

    View full-size slide

  60. Conventional Commits

    View full-size slide

  61. Keepachangelog

    View full-size slide

  62. SemVer
    MAJOR.MINOR.PATCH

    Breaking!
    New Feature!
    Bug
    fi
    x!

    View full-size slide

  63. • Have a stable CI
    • 3rd-party dependency bumps
    Maintain the Codebase

    View full-size slide

  64. • Have a stable CI
    • 3rd-party dependency bumps
    Maintain the Codebase

    View full-size slide

  65. • Have a stable CI
    • 3rd-party dependency bumps
    Maintain the Codebase

    View full-size slide

  66. • Stalebot
    • Code of
    Conduct
    • Interaction
    Limits
    • Danger
    Maintain the Repository

    View full-size slide

  67. Danger & PR Automations

    View full-size slide

  68. Danger & PR Automations

    View full-size slide

  69. Danger & PR Automations

    View full-size slide

  70. Danger & PR Automations

    View full-size slide

  71. Danger & PR Automations

    View full-size slide

  72. Understand your
    growth model

    View full-size slide

  73. Growth
    High User Growth Low User Growth
    High Contributor Growth
    Low Contributor Growth

    View full-size slide

  74. High User Growth Low User Growth
    High Contributor Growth
    Federations
    (Flutter)
    Low Contributor Growth
    Growth

    View full-size slide

  75. High User Growth Low User Growth
    High Contributor Growth
    Federations
    (Flutter)
    Low Contributor Growth
    Stadium
    (OkHTTP)
    Growth

    View full-size slide

  76. High User Growth Low User Growth
    High Contributor Growth
    Federations
    (Flutter)
    Clubs
    (Arrow)
    Low Contributor Growth
    Stadium
    (OkHTTP)
    Growth

    View full-size slide

  77. High User Growth Low User Growth
    High Contributor Growth
    Federations
    (Flutter)
    Clubs
    (Arrow)
    Low Contributor Growth
    Stadium
    (OkHTTP)
    Toys
    Growth

    View full-size slide

  78. • First, what’s your model?
    • Start a chat channel
    • KotlinLang Slack
    • Discord
    • Twitter pro
    fi
    le
    Grow your community

    View full-size slide

  79. • Run contests
    • Join Hacktoberfest
    • Organize Meet-ups
    • Swag-as-a-service
    Gamify your contributions

    View full-size slide

  80. • Github Sponsors
    • OpenCollective
    • thanks.dev
    Grow your sponsors

    View full-size slide

  81. PAOLO ROTOLO
    @paolorotolo
    NICOLA CORTI
    @cortinico
    Thank you!

    View full-size slide