재직 중인 회사에서 apk로 배포하던걸 bundle로 배포하게 됐습니다. 그러면서 기존에 내부에서 테스트를 위해서 사용하던 Fabric Beta(apk파일만 지원)를 사용할 수 없게 됐습니다. 그래서 내부적으로 Fabric Beta와 비슷한 형식의 시스템을 구현하게 됐습니다.

 기능을 구현하기 위해서 사용한 기술들로는 플레이스토어 내부 앱 공유, Firebase의 Cloud Store, Functions, Custom Gradle Plugin을 개발해서 진행했습니다.

자동화(?)된 플로우를 설명해드리면, 

bundle로 앱 빌드 -> 구글 내부 앱 공유 api를 통해서 업로드 -> 리턴 받은 download Url을 firebase functions로 구현된 api 호출 -> functions에서 Cloud Store에 업로드하는 api 호출 -> Slack 특정 채널에 압축된 파일 업로드입니다. 이 순서로 task들이 자동으로 실행돼 저희가 개발한 beta 앱에서 확인을 할 수 있게 됩니다. 앱의 경우 Firebase Cloud Store에 저장된 데이터를 보여주는 단순한 앱입니다.

bundle 빌드, functions에서 Cloud Store호출을 제외한 모든 부분은 Custom Gradle Plugin을 개발해서 진행됐습니다.

 

그럼 여기서 Custom Gradle Plugin이란 무엇이고, 어떻게 만들 수 있는지 설명하겠습니다.

Gradle Plugin?

안드로이드를 개발하게 되면 Gradle이란 빌드 툴을 사용하게 됩니다. 플러그인을 프로젝트에 적용하면 플러그인이 프로젝트 기능을 확장할 수 있습니다.  

 

여기서 저희는 이미 아래와 같은 플러그인을 적용해서 사용하고 있었습니다.

apply plugin: 'com.android.application'

 

아래와 같이 작업을 할 수 있습니다.

android {
    compileSdkVersion 29
    defaultConfig {
        applicationId "com.mozzet.diffu"
        minSdkVersion 22
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        debug {
            applicationIdSuffix ".debug"
        }

        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

 

Android 관련된 정보는 아래 두 링크에서 확인 가능합니다.

https://developer.android.com/studio/build

http://google.github.io/android-gradle-dsl/current/

Custom Plugin 만드는 방법들

  • Build script

    • 현재 작성된 빌드 스크립트 내에 추가적으로 작성하는 방법입니다. 이 방법은 따로 작업을 해줄 필요가 없어서 편합니다. 하지만 재사용이 불가능합니다.

  • buildSrc project

    • 플러그인을 rootProjectDir/buildSrc/src/main/groovy (java or kotlin) 디렉터리에 넣고 작성하여서 프로젝트 내 다른 빌드 스크립트에서 사용할 수 있습니다. 첫 번째 방식과 차이점은 프로젝트 내에 모든 빌드 스크립트에서 사용이 가능하다는 점입니다. 하지만 외부에서는 재사용이 불가능합니다.

  • Standalone project

    • 플러그인을 별도의 프로젝트로 만들어 개발할 수 있습니다. 이 경우 다른 프로젝트에서 사용이 가능하고, 다른 사용자와도 공유할 수 있습니다.

이제 각각의 생성 방법에 대해서 알아보겠습니다. 저는 kotlin 및 kotlin DSL을 이용해서 개발했습니다.

1. Build script 이용해서 플러그인 개발

간단하게 아래의 코드를 추가하면 끝납니다.

build.gradle.kts

class TaskInBuildScriptPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        project.task("taskInBuildScript") {
            group = "custom plugin task group"
            doLast {
                println("Hello from the taskInBuildScript")
            }
        }
    }
}

// Apply the plugin
apply<TaskInBuildScriptPlugin>()

추가적으로 필요한 설정을 extenstion을 이용해서 설정을 할 수 있습니다. 위에 코드에서 익스텐션을 추가한 코드입니다.

open class TaskInBuildScriptPluginExtension {
    var name: String? = null
}

class TaskInBuildScriptPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        val extension = project.extensions.create<TaskInBuildScriptPluginExtension>("buildScript")
        project.task("taskInBuildScript") {
            group = "custom plugin task group"
            doLast {
                println("${extension.name}, Hello from the taskInBuildScript")
            }
        }
    }
}

// Apply the plugin
apply<TaskInBuildScriptPlugin>()

// Configure the extension using a DSL block
configure<TaskInBuildScriptPluginExtension> {
    name = "sangcomz"
}

-결과 화면-

 

2. buildSrc project

프로젝트 내부에 아래와 같이 만들어 줍니다.

아래 3개의 파일을 추가해줍니다.

 

TaskInBuildSrcPlugin.kt

class TaskInBuildSrcPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        project.task("taskInBuildSrc") {
            group = "custom plugin task group"
            doLast {
                println("Hello from the taskInBuildScript")
            }
        }
    }
}

buildSrc/build.gradle.kts

plugins {
    `kotlin-dsl`
}

repositories {
    jcenter()
}

resources/META-INF/task-in-buildsrc-plugin.properties

implementation-class=TaskInBuildSrcPlugin

 

사용한 부분의 build.gradle.kts에 생성한 플러그인을 추가해줍니다.

plugins {
    kotlin("jvm") version "1.3.61"
    //apply buildSrc plugin
    `task-in-buildsrc-plugin`
}

 

-결과 화면-

동일하게 Extension을 추가해보겠습니다. 플러그인 자체에 extension을 추가하는 코드는 위에 코드를 참고해주세요.

 

TaskInBuildSrcPluginExtension.kt

open class TaskInBuildSrcPluginExtension(var name: String? = null)

플러그인이 적용된 build.gradle.kts에 아래와 같은 코드 블록을 추가해서 값들을 설정해줍니다.

//buildSrc extension
buildSrc{
    name = "sangcomz"
}

-결과 화면-

3.Standalone Project

이번엔 제가 실제로 사용을 위해 만든 plugin을 이용해서 설명을 하겠습니다. (슬쩍 홍보..)

https://github.com/sangcomz/gradle-slack-upload-plugin

 

sangcomz/gradle-slack-upload-plugin

📦 Easily upload files to Slack using Gradle Plugin - sangcomz/gradle-slack-upload-plugin

github.com

via GIPHY

먼저 프로젝트를 생성해줍니다.

이 경우에 buildSrc를 이용해서 plugin을 생성하는 것과 비슷합니다.

저는 이런 식으로 생성을 해줬습니다. 최상위 패키지가 buildSrc와 src의 차이만 있습니다. 여기서 중요한 부분은 배포를 해줘야 한다는 점입니다.

 

일단 저는 로컬에 배포하는 방식으로 설명을 드리겠습니다.

실제 maven 저장소에 올리는 방법은 제 샘플 코드 혹은 추가적으로 포스팅하도록 하겠습니다. 제가 이 글을 수정해야 할 이유는 한 가지로만....

 

build.gradle.kts

plugins {
    kotlin("jvm") version "1.3.61"
    `kotlin-dsl`
    maven
    `maven-publish`
    id("com.jfrog.bintray") version "1.8.4"
}

group = "xyz.sangcomz"
version = "0.0.3"

repositories {
    mavenCentral()
    jcenter()
}

dependencies {
    implementation(kotlin("stdlib-jdk8"))
    implementation("com.github.kittinunf.fuel:fuel:2.2.1")
}

tasks {
    compileKotlin {
        kotlinOptions.jvmTarget = "1.8"
    }
    compileTestKotlin {
        kotlinOptions.jvmTarget = "1.8"
    }
}

tasks {
    "uploadArchives"(Upload::class) {
        repositories {
            withConvention(MavenRepositoryHandlerConvention::class) {
                mavenDeployer {
                    withGroovyBuilder {
                        "repository"("url" to uri("repo")) {
                        }
                    }
                }
            }
        }
    }
}

이제 uploadArchives task를 실행해줍니다. 그렇게 되면 repo라는 폴더가 생성됩니다.

그러면 이제 이 주소에 있는 gradle plugin을 사용해보도록 하겠습니다.

 

build.gradle.kts

buildscript {
    repositories {
        maven {
            url = uri("/Users/sangcomz/projects/gradle-slack-upload-plugin/repo")
        }
    }

    dependencies {
        classpath("xyz.sangcomz:gradle-slack-upload-plugin:0.0.4")
    }
}

apply {
    plugin("xyz.sangcomz.gradle")
}

적용을 하게 되면 slack file upload라는 그룹으로 생성된 것을 확인할 수 있습니다.

Standalone의 경우 동일하게 extension을 사용할 수 있습니다.

configure<xyz.sangcomz.gradle.SlackUploadPluginExtension>{
    token = "your api token"
    channels = "wowchannel"
    title = "File Title"
    initialComment = "Upload Sample.txt"
    filePaths =  arrayOf("sample.txt")
    zipName = "wowUploada"
    zipFilePath = "build/zip"
    deleteZipFileAfterUpload = false
}

 

간단하게 세 가지 방법에 대해서 알아봤습니다. 더 자세하고 groovy를 이용한 방법을 확인하고 싶으시다면 아래의 링크에서 확인이 가능합니다. 영어라는 점을 빼면 훨씬 좋은 공식 문서입니다.

https://docs.gradle.org/current/userguide/custom_plugins.html

 

Developing Custom Gradle Plugins

In this example, GreetingPluginExtension is a object with a property called message. The extension object is added to the project with the name greeting. This object then becomes available as a project property with the same name as the extension object. O

docs.gradle.org

추가적으로 궁금한 부분이 있으면 댓글로 남겨주세요. 

감사합니다.

'dev > gradle' 카테고리의 다른 글

Make Custom Gradle Plugin for build  (0) 2020.02.20

via GIPHY

https://medium.com/@sangcomz/dagger2%EB%9E%80-f6aed5948023

 

Dagger 2란?

Dagger 2

medium.com

2016년 3월에 이런 글을 썼었습니다. 쓴 이유는 회사에서 Dagger2를 사용하고 있었기 때문에 학습 및 발표를 위해서 작성했었습니다.

한 동안 Dagger2를 사용하지 않고 전 회사에선 Koin을 사용하고, 요즈음엔 어떤 DI 쓰고 있지 않아서 기억이 가물가물해졌습니다. (애초에.... 제대로 이해하지 못했습니다.....)

 

이번에 Dagger2를 사용할 일이 있어서 다시 학습을 했습니다.

학습을 하면서 제가 저 글에서 알았던 지식이 잘 못 됐다는 것을 깨달았습니다.

https://developer.android.com/training/dependency-injection

 

Dependency injection in Android  |  Android Developers

Dependency injection (DI) is a technique widely used in programming and well suited to Android development. By following the principles of DI, you lay the groundwork for good app architecture. Implementing dependency injection provides you with the followi

developer.android.com

https://codelabs.developers.google.com/codelabs/android-dagger/#13

 

Using Dagger in your Android app

In Android, you usually create a Dagger graph that lives in your Application class because you want an instance of the graph to be in memory as long as the app is running. In this way, the graph is attached to the app's lifecycle. In our case, we also want

codelabs.developers.google.com

위에 두 링크를 통해서 학습을 했고, 학습을 하면서 정리한 내용을 간단히 공유 혹은 메모를 하려고 합니다.

 

의존성 주입이란?

의존성 주입은 말 그대로 외부에서 의존성을 주입받는 것입니다. 예를 들어서 Car라는 Class는 Engine 클래스에 대한 참조가 필요합니다. 이때 Car 클래스는 Engine 클래스에 대한 의존성을 갖게 됩니다. 이때 Engine 클래스 인스턴스를 주입받는 것을 말합니다. 

의존성 주입 장점

a. 코드 재사용성

b. 리팩토링을 더 쉽게

c. 테스트를 더 쉽게

의존성 주입 방법

a. 생성자 주입 (Constructor Injection)

class Car(private val engine: Engine) {
    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val engine = Engine()
    val car = Car(engine)
    car.start()
}

b. 필드 주입 (Field Injection or Setter Injection)

class Car {
    lateinit var engine: Engine

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.engine = Engine()
    car.start()
}

 

의존성 주입 자동화의 필요성

For big apps, taking all the dependencies and connecting them correctly can require a large amount of boilerplate code. In a multi-layered architecture, in order to create an object for a top layer, you have to provide all the dependencies of the layers below it. As a concrete example, to build a real car you might need an engine, a transmission, a chassis, and other parts; and an engine in turn needs cylinders and spark plugs.

앱이 커지면 많은 수의 의존성 주입이 생기는데, 이는 많은 보일러 플레이트 코드를 만듭니다. 그리고 상위 레이어에서 하위 레이어까지 주입을 하게 되면 오류를 낳게 됩니다.

When you're not able to construct dependencies before passing them in — for example when using lazy initializations or scoping objects to flows of your app — you need to write and maintain a custom container (or graph of dependencies) that manages the lifetimes of your dependencies in memory.

메모리 관리가 어렵습니다.

 

자동화하는 라이브러리엔 두 가지 방식이 있습니다.

- 리플렉션을 이용해서 런타임에 의존성을 주입해주는 라이브러리 (ex:Guice)

- 코드를 생성해서 컴파일 타임에 의존성을 연결해주는 라이브러리 (ex:Dagger)

 

의존성 주입 대안

서비스 로케이터 패턴을 사용해서 의존성 주입

 

서비스 로케이터 패턴의 단점

- 테스트하기 더 어려움

- 런타임에 오류를 발견

- 객체의 수명 관리 어려움

 

https://en.wikipedia.org/wiki/Service_locator_pattern

 

Service locator pattern - Wikipedia

The service locator pattern is a design pattern or anti-pattern used in software development to encapsulate the processes involved in obtaining a service with a strong abstraction layer. This pattern uses a central registry known as the "service locator",

en.wikipedia.org

 

Dagger 기초

Dagger 장점

- 의존성 컨테이너 생성해서 의존성 관리

- 필요한 클래스의 팩토리 생성

- Scope를 이용해서 의존성 재사용 및 재생성을 제어

- SubComponent를 이용해서 더 이상 필요 없는 의존성 혹은 재사용할 의존성을 관리

 

Dagger는 annotation processing을 사용해서 컴파일 타임에 수동 의존성 주입과 비슷한 코드를 생성해줍니다.

그리고 컴파일 타임에 종속성 그래프를 빌드하고 검증해줍니다. 

- 런타임 오류가 발생하지 않습니다.

- 의존성 싸이클이 발생하는 걸 방지합니다.

 

 @Inject 사용

// @Inject Dagger에게 이 오브젝트 인스턴스를 어떻게 생성하는지 알려줍니다.
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

@Component 사용

// @Component Dagger에게 의존성 컨테이너를 생성하도록 말해줍니다.
@Component
interface ApplicationGraph {
    // 필요한 클래스의 인스턴스를 반환하는 함수를 작성
    fun repository(): UserRepository
}

Scoping with Dagger

 

You can use scope annotations to limit the lifetime of an object to the lifetime of its component. This means that the same instance of a dependency is used every time that type needs to be provided.

Scope Annotation을 이용해서 생성된 인스턴스의 생명주기를 설정할 수 있습니다.

 

Android App에서 Dagger 사용

Best practices summary

- 대부분의 경우에 가능하면 생성자 @Inject를 사용합니다. 예외인 경우에 @Binds 및 @Provides를 사용합니다.

 

@Binds : 주입하는 대상이 interface일 경우에 사용합니다

interface NeedBind {
    fun print()
}

class NeedBindImpl @Inject constructor() : NeedBind {
    override fun print() {
        println("Hello NeedBindImpl")
    }
}

@Module
abstract class NeedBindModule {
    @Binds //NeedBind 인스턴스할 방법을 Dagger에게 알려줍니다.
    abstract fun provideNeedBind(needBindImpl: NeedBindImpl): NeedBind
}

@Provides : 프로젝트가 소유하지 않은 클래스를 제공하는 방법을 Dagger에게 알려줘야 할 때 사용합니다. (ex:Retrofit)

@Module
class NetworkModule {

    // @Provides 대거에게 인스턴스를 어떻게 생성하는지 알려줍니다.
    @Provides
    fun provideMainApiService(): MainApiService {
        return Retrofit.Builder()
            .baseUrl("https://example.com")
            .build()
            .create(MainApiService::class.java)
    }
}

 

- 컴포넌트는 한 번만 모듈에 선언

 

- Custom Scope Animation은 생명주기에 맞는 이름으로 만들면 재사용하기 좋습니다.

 

Dagger subcomponents

로그인을 구현할 때, 로그인 플로우상 같은 인스턴스를 사용해야 하는 인스턴스가 존재할 수 있습니다. 이럴 때 @Singleton을 사용하면 동일한 인스턴스를 받을 수 있습니다. 하지만 여기선 두 가지 문제점이 있습니다.

- 로그인 플로우가 끝난 뒤에도 메모리에 남아있게 됩니다.

- 새로운 로그인 플로우가 시작됐을땐 새로운 인스턴스가 필요한데, 기존에 생성한 인스턴스를 사용하게 됩니다.

 

그럴 때 subcomponents를 사용해서 문제를 해결할 수 있습니다. subcomponents는 캡슐화의 좋은 방법입니다.

 

// Subcomponent를 생성해줍니다.
@ActivityScope
@Subcomponent
interface SubComponent {

    @Subcomponent.Factory
    interface Factory {
        fun create(): SubComponent
    }

    fun inject(nextActivity: NextActivity)
}

// Subcomponent를 갖고 있는 Module을 만들어줍니다.
@Module(subcomponents = [SubComponent::class])
class SubModule {}

@Singleton
@Component(modules = [NetworkModule::class, NeedBindModule::class])
interface AppComponent {
    fun inject(mainActivity: MainActivity)

    fun loginComponent(): SubComponent.Factory //subcomponent 생성 방법을 Dagger에게 알려줍니다.
    
    
}

// 사용합니다.
class NextActivity : AppCompatActivity() {

    lateinit var subComponent: SubComponent

    @Inject
    lateinit var nextViewModel: NextViewModel

    override fun onCreate(savedInstanceState: Bundle?) {

        subComponent = (application as Dagger2SampleApp).appComponent.loginComponent().create()
        subComponent.inject(this)

        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_next)

        nextViewModel.print()
    }
}

 

추가적으로 알면 좋은 Annotation 설명

@BindsInstance

Dagger Graph 외부에서 이미 만들어진 객체를 Graph에 넣어줄 때 사용합니다. (Ex:Context)

@Singleton
@Component(modules = [NetworkModule::class, NeedBindModule::class, CarBindingModule::class, AnimalBindingModule::class])
interface AppComponent {
    fun inject(mainActivity: MainActivity)
    @Component.Factory
    interface Factory {
        // With @BindsInstance, Context를 의존성 그래프에서 사용할 수 있게 됩니다.
        // 이미 instance가 외부에서 생성된 것중에 의존성 그래프에서 필요할 때 사용합니다.
        fun create(@BindsInstance context: Context): AppComponent
    }

    fun loginComponent(): SubComponent.Factory
}

@IntoMap

Map으로 주입 받을 수 있게 만들어줍니다.

@Module
abstract class AnimalBindingModule {

    @Binds
    @IntoMap
    @StringKey("Dog")
    abstract fun provideDog(dog: Dog): Animal


    @Binds
    @IntoMap
    @StringKey("Cat")
    abstract fun provideCat(cat: Cat): Animal
}

class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var animalMap: Map<String, @JvmSuppressWildcards Animal>
    
    override fun onCreate(savedInstanceState: Bundle?) {
    	(application as Dagger2SampleApp).appComponent.inject(this)
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        .
        .
        .
        animalMap.forEach {
            print("I am ${it.key} ")
            it.value.cry()
        }
    }
}

 

@IntoSet

Set으로 주입 받을 수 있게 만들어줍니다.

@Module
abstract class CarBindingModule {

    @Binds
    @IntoSet
    abstract fun provideSuperCar(car: SuperCar): Car


    @Binds
    @IntoSet
    abstract fun provideMiniCar(car: MiniCar): Car
}



class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var carSet: Set<@JvmSuppressWildcards Car>
    
    override fun onCreate(savedInstanceState: Bundle?) {
    	(application as Dagger2SampleApp).appComponent.inject(this)
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        .
        .
        .
        carSet.forEach {
        	it.boooong()
        }
    }
}

 

@IntoMap의 경우에 Android AAC ViewModel을 주입할 수 있습니다.

@Singleton
class ViewModelFactory @Inject constructor(private val viewModels: MutableMap<Class<out ViewModel>, Provider<ViewModel>>) :
    ViewModelProvider.Factory {

    override fun <T : ViewModel> create(modelClass: Class<T>): T =
        viewModels[modelClass]?.get() as T
}

@Target(
    AnnotationTarget.FUNCTION,
    AnnotationTarget.PROPERTY_GETTER,
    AnnotationTarget.PROPERTY_SETTER
)
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
@MapKey
internal annotation class ViewModelKey(val value: KClass<out ViewModel>)

@Module
abstract class ViewModelModule {

    @Binds
    internal abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory

    @Binds
    @IntoMap
    @ViewModelKey(HomeViewModel::class)
    internal abstract fun homeViewModel(viewModel: HomeViewModel): ViewModel
}

 

전체 코드는 아래 저장소에서 확인 가능합니다.

https://github.com/sangcomz/Dagger2Sample

 

sangcomz/Dagger2Sample

Contribute to sangcomz/Dagger2Sample development by creating an account on GitHub.

github.com

 

 

1.LayoutManager?

A LayoutManager is responsible for measuring and positioning item views within a RecyclerView as well as determining the policy for when to recycle item views that are no longer visible to the user. By changing the LayoutManager a RecyclerView can be used to implement a standard vertically scrolling list, a uniform grid, staggered grids, horizontally scrolling collections and more. Several stock layout managers are provided for general use.

Android Developer 사이트에서 확인할 수 있는 LayoutManager의 설명입니다.
간단히 설명을 드리면 RecyclerView에 그려지는 View의 크기 및 위치를 정하고, View 재활용을 담당하고 있습니다.
기본적으로 제공되고 있는 LayoutManager는 LinearLayoutManager, GridLayoutManager, StaggeredGridLayoutManager가 있습니다.

2.LayoutManager Method

많은 함수들이 존재합니다. 이중에 오늘 CardStack View를 만들때 필요한 함수들에 대해서만 알아보겠습니다.

generateDefaultLayoutParams()

RecyclerView에 기본 LayoutPararm을 생성하는 작업을 합니다.

구현 코드

override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {
    return RecyclerView.LayoutParams(
        ViewGroup.LayoutParams.MATCH_PARENT,
        ViewGroup.LayoutParams.MATCH_PARENT
    )
}

onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State)

이 함수에서 실제 배치될 View를 그려주는 작업을 하게 됩니다.

먼저 이 함수에서는 파라미터 두 개를 받고 있는 것을 알 수 있습니다.
recycler, state입니다.

recycler

RecyclerView에서 아주 중요한 역할을 하는 친구라는걸 이름에서 알 수 있습니다.

A Recycler is responsible for managing scrapped or detached item views for reuse.

scrapped 또는 detached된 View를 재사용하기 위해서 그 View들을 관리해주는 역할을 해준다고 합니다.

  • scrapped : View가 RecyclerView에 붙어있지만 재사용 혹은 제거를 위해 마킹을 한 상태
  • detached : View가 RecyclerView에서 detached된 상태

state

말 그대로 RecyclerView의 상태값입니다.
그리고 내부 컴포넌트끼리 데이터 통신을 위해서 사용되기도 한다고 합니다.

구현 코드

override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
    detachAndScrapAttachedViews(recycler) // 1
    if (itemCount < 1) return

    val startPosition = (itemCount - 1).coerceAtMost(topPosition + preloadCount - 1)

    for (position in startPosition downTo topPosition) {
        val isTopView = position == topPosition
        val drawPosition = position - topPosition
        val view = recycler.getViewForPosition(position)

        changeDefaultElevation(ViewCompat.getElevation(view))

        if (isTopView) setTopViewOnTouchListener(view)

        setElevation(
            view,
            if (isTopView) defaultElevation
            else defaultElevation - 1f * drawPosition
        )

        measureChildWithMargins(view, 0, 0)

        val params = view.layoutParams as RecyclerView.LayoutParams

        layoutDecorated(
            view,
            params.leftMargin + paddingLeft,
            params.topMargin + paddingTop,
            getDecoratedMeasuredWidth(view) + params.leftMargin + paddingLeft,
            getDecoratedMeasuredHeight(view) + params.topMargin + paddingTop
        ) // 2

        if (drawPosition > 0) {
            view.scaleX = validateScale(1 - scaleGap * drawPosition)
            view.scaleY = validateScale(1 - scaleGap * drawPosition)
        } else {
            view.scaleX = validateScale(1f)
            view.scaleY = validateScale(1f)
        }

        if (startXPosition == null || startYPosition == null) {
            initStartPosition(view.x, view.y)
        }

        addView(view) // 3
    }
}

1 : 현재 붙어있는 View들을 scrapped 상태로 변경해줍니다.
2 : view의 left, top, right, bottom 값을 정해줍니다.
3 : View를 실제로 붙여줍니다.

3.결과

 


LayoutManager를 이용하면 RecylcerView에서 다양하게 View를 표현 할 수 있습니다.
다음 포스트에선 이번 포스트에서 스리슬쩍 넘어간 Recycler에 대해서 더 알아보도록하겠습니다.
Github에서 전체 코드를 확인해보세요!

 

1. Use hard-coded text

hard-coded text를 이용할 수 있습니다.

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{`It is hard-coded.`}"/>

Result

It is hard-coded.

2. Use string resource

strings.xml에 선언된 string을 이용할 수 있습니다.

<string name="msg_hello_data_binding">Hello DataBinding!</string>
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{@string/msg_hello_data_binding}"/>

Result

Hello DataBinding!

3. Use format string resource with hard-coded text

strings.xml에 선언된 format string을 이용할 수 있습니다.

<string name="msg_hello_data_binding">%s, Hello DataBinding!</string>
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{@string/msg_hello_data_binding(`Seokwon`)}"/>

Result

Seokwon, Hello DataBinding!

4. Use format string resource with string resource

물론 format string에 hard-coded뿐만 아니라 string resource도 사용할 수 있습니다.

<string name="msg_hello_data_binding">%s, Hello DataBinding!</string>
<string name="everybody">Everybody</string>
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{@string/msg_hello_data_binding(@string/everybody)}"/>

Result

Everybody, Hello DataBinding!

5. Use format string resource with ternary operator ?:(Feat hard-coded)

format string resource 에서 삼항 조건 연산자?:와 hard-coded text를 이용해서 표현할 수 있습니다.

<string name="msg_hello_data_binding">%s, Hello DataBinding!</string>
<data>
    <variable
        name="isHelpful"
        type="Boolean"/>
    <data>

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{@string/msg_hello_data_binding(isHelpful ? `Gooood` : `Woo`)}"/>

Result

Gooood, Hello DataBinding!
or
Woo, Hello DataBinding!

6. Use format string resource with ternary operator ?:(Feat string resource)

format string resource 에서 삼항 조건 연산자?:와 string resource를 이용해서 표현할 수 있습니다.

<string name="msg_hello_data_binding">%s, Hello DataBinding!</string>
<string name="msg_good">Goooood</string>
<string name="msg_bad">Wooooooooo</string>
<data>
    <variable
        name="isHelpful"
        type="Boolean"/>
    <data>

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{@string/msg_hello_data_binding(isHelpful ? @string/msg_good : @string/msg_bad)}"/>

Result

Goooood, Hello DataBinding!
or
Wooooooooo, Hello DataBinding!

 

 

 

sangcomz/DatabindingExample

Contribute to sangcomz/DatabindingExample development by creating an account on GitHub.

github.com

 

+ Recent posts