728x90

저희 프로젝트는 협업할 때 main branch로 merge 되기 전에 자동으로 test, lint check, build 되는지 확인하도록 했습니다.

github actions을 통해 ktlint, debug.keystore, google-services.json을 관리하며 CI(Continuous Integration) 구축을 한 방법에 대해 작성해보겠습니다.

CI 환경 설정

Github Actions 사용

저희는 github를 이용하여 개발을 진행했기에 쉽게 사용할 수 있는 github actions를 사용하였습니다.
dev, main branch로 병합되기 전에 build, ktlint가 성공해야지 병합될 수 있도록 했습니다

build.yml 파일을 작성하여 pull_request 이벤트가 발생하면 빌드 작업이 실행되도록 했습니다.

name: Build

on:
  pull_request:
    branches: [ "main", "dev" ]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'
          cache: gradle

      - name: Gradle Caching
        uses: actions/cache@v4
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
          restore-keys: |
            ${{ runner.os }}-gradle-

      - name: Grant execute permission for gradlew
        run: chmod +x gradlew

      - name: Run Build
        run: ./gradlew build --no-daemon

ktlint 관리

팀원들끼리 같은 코틀린 스타일 규칙을 적용하는 것이 필요했습니다. 참고: lint적용

그래서 저희는 ktlint를 도입하여 팀원들이 작성한 코드가 일관된 컨벤션 규칙을 따르도록 강제했습니다.
ktlint_check.yml 파일을 작성해 pull_request 이벤트 발생시 workflow이벤트가 실행되도록 했습니다.

name: ktlint chcek

on:
  pull_request:
    branches: [ "main", "dev" ]

jobs:
  lint:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'
          cache: gradle

      - name: Gradle Caching
        uses: actions/cache@v4
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
          restore-keys: |
            ${{ runner.os }}-gradle-

      - name: Grant execute permission for gradlew
        run: chmod +x gradlew

      - name: Run ktlintCheck
        run: ./gradlew ktlintCheck --no-daemon

저희는 이 두개의 workflow를 통과해야지만 merge 될 수 있도록 했습니다.

로그인 구현할 때의 문제점

저희는 firebase를 사용하여 로그인을 구현하면서 google-services.json*.keystore를 읽어오는 작업이 필요했습니다.
하지만 이 두개는 공개된 github repository에 올리면 안됬습니다.

이를 해결하기위해 setting > security > secrets and variables > actions에서 secret 값을 추가해줬습니다.
그리고 debug.keystore는 올릴 수 없었기에 Window powershell에서 base64로 인코딩 해줬습니다.

[Convert]::ToBase64String((Get-Content -Path .\debug.keystore -Encoding Byte)) > debug.keystore.base64

image

그리고 app모듈의 build.gradle.kts에서 keystore를 읽는 부분을 수정하였습니다.

getByName("debug") {
    val keystoreProperties = Properties().apply {
        val file = rootProject.file("local.properties")
        if (file.exists()) {
            load(file.inputStream())
        }
    }
    val keyStorePath = keystoreProperties.getProperty("DEBUG_KEYSTORE_PATH")
    storeFile = if (keyStorePath != null) {
        file(keyStorePath)
    } else {
        file("${System.getProperty("user.home")}/.android/debug.keystore")
    }
}

또한 debug.keystore를 읽을 수 있도록 workflow도 수정해줬습니다.

최종 코드

ktlint_check.yml

name: ktlint chcek

on:
  pull_request:
    branches: [ "main", "dev" ]

jobs:
  lint:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'
          cache: gradle

      - name: Gradle Caching
        uses: actions/cache@v4
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
          restore-keys: |
            ${{ runner.os }}-gradle-

      - name: Grant execute permission for gradlew
        run: chmod +x gradlew

      - name: Decode Google Services JSON
        env:
          GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }}
        run: echo "$GOOGLE_SERVICES_JSON" > ./app/google-services.json

      - name: Run ktlintCheck
        run: ./gradlew ktlintCheck --no-daemon

build.yml

name: Build

on:
  pull_request:
    branches: [ "main", "dev" ]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'
          cache: gradle

      - name: Gradle Caching
        uses: actions/cache@v4
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
          restore-keys: |
            ${{ runner.os }}-gradle-

      - name: Grant execute permission for gradlew
        run: chmod +x gradlew

      - name: Decode Google Services JSON
        env:
          GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }}
        run: echo "$GOOGLE_SERVICES_JSON" > ./app/google-services.json

      - name: Decode Debug Keystore
        env:
          DEBUG_KEYSTORE_BASE64: ${{ secrets.DEBUG_KEYSTORE_BASE64 }}
        run: |
          mkdir -p $HOME/.android
          echo "$DEBUG_KEYSTORE_BASE64" | base64 -d > $HOME/.android/debug.keystore

      - name: Run Build
        run: ./gradlew build --no-daemon

위와 같이 작성하니 CI 작업이 잘 실행되었습니다!

728x90

'안드로이드' 카테고리의 다른 글

desugaring이란?  (0) 2024.12.23
안드로이드 XML Naming Convention  (1) 2024.09.04
안드로이드의 논리적인 단위  (0) 2024.08.29
728x90

desugaring이란?

desugaring을 한국어로 번역하면 설탕을 없애다 뜻이다.

프로그래밍에서는 syntatic sugar를 제거하여 기초적인 형태로 변환하는 과정을 의미한다.

Python을 예로 들어보면 list comprehension 이 있다.

다음과 같은 코드가 있다고 해보자

squares = [x**2 for x in range(5)]

이 코드는 0부터 4까지의 숫자를 제곱한 [0, 1, 4, 9, 16] 의 값을 반환한다.

그렇지만 이 문법을 일반 for loop 로도 바꿀 수 있다.

squares = []
for x in range(5):
    squares.append(x**2)

list comprehensionsyntatic sugar 역할을 하여 코드가 간결해보이지만 반복문을 사용하여 리스트를 생성하는 방식으로 변환될 수 있는 것이다.

이처럼 안드로이드에서의 desugaring도 마찬가지의 의미이다.

Android SDK24 에서는 java8의 일부 기능만 지원을 하고 어떤 것은 호환이 되지 않는다. 안드로이드 개발에서 사용하는 Java 버전은 항상 최신 버전을 따라가지 않기 때문에 이 기능을 호환이 가능하도록 해줘야 한다.

java8의 주요 기능으로는 Lambda Expression, Method References, java.time 등이 있다.

desugaring 과정은 아래 사진과 같이 진행된다.

image

AGP는 java8의 일부 기능을 기본적으로 지원해준다.

하지만 java.time 과 같은 기능을 사용하려면 Java 기능을 더 기본적인 형태로 변환하는 bytecode 변환 과정을 거쳐야한다.

이렇게 변환된 코드는 안드로이드가 지원하는 DEX 코드로 컴파일되어서 하위 버전의 안드로이드에서도 실행될 수 있다.

이 AGP의 desugar과정은 D8/R8 Compiler 수행한다. D8/R8 Compiler는 안드로이드의 byte코드를 DEX코드로 컴파일 하는 도구이다. 이 과정에서 java8 기능을 사용 가능하도록 변환해주기 때문에 자유롭게 사용할 수 있다.

설정도 매우 간단하다.

android {
    defaultConfig {
        // Required when setting minSdkVersion to 20 or lower
        multiDexEnabled = true
    }

    compileOptions {
        // Flag to enable support for the new language APIs

        // For AGP 4.1+
        isCoreLibraryDesugaringEnabled = true
        // For AGP 4.0
        // coreLibraryDesugaringEnabled = true

        // Sets Java compatibility to Java 8
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
}

dependencies {
    // For AGP 7.4+
    coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3")
    // For AGP 7.3
    // coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.2.3")
    // For AGP 4.0 to 7.2
    // coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.9")
}

이렇게 설정을 해주고 gradle sync를 해주면 된다.

공식문서에서는 뭔가 java8버전에 대한 설명이 대부분이여서 java8까지만 지원하는 것 같아서 테스트를 진행해봤다.

sourceCompatibilitytargetCompatibility 그리고 kotlin jvmTarget을 17로 바꿔보고서 진행을 했다.

아래와 같이 Java14버전부터 나오고 17부터 안정화된 switch case 문을 작성하였다.

Test.java

public class Test {
    public static int getDayValue(String day) {
        return switch (day) {
            case "MONDAY", "FRIDAY", "SUNDAY" -> 6;
            case "TUESDAY" -> 7;
            default -> 0;
        };
    }
}

그리고서 이 코드를

@Composable
fun Greeting(
    name: String,
    modifier: Modifier = Modifier,
) {
    val x = Test.getDayValue("MONDAY")
    Column {
        Text(
            text = "Hello $name!",
            modifier = modifier,
        )
        Text(
            text = "Hello $x!",
            modifier = modifier,
        )
        Text(
            text = "Hello $name!",
            modifier = modifier,
        )
        Button(
            onClick = {
                Log.d("TAG", "Button clicked")
            },
            modifier = modifier,
        ) {
            Text("Click me")
        }
    }
}

이렇게 사용해봤다.

이렇게 한뒤 API 26으로 실행결과

image

문제 없이 잘 나오는 것을 확인할 수 있었다.

마찬가지로 java17 버전부터 나온 record 문법을 사용해도 빌드는 잘 되는 것을 확인했다.

그런데 여러가지 실험을 하는 중에 java17 문법부터 추가된 stream.toList()가 실행이 안되는 것을 알 수 있었다. 이 부분에 대해서는 추측이지만 17버전 문법도 전부다 지원하지는 않는 것 같다.

 

Reference
https://developer.android.com/studio/write/java8-support

728x90

'안드로이드' 카테고리의 다른 글

android CI 적용기  (1) 2024.12.23
안드로이드 XML Naming Convention  (1) 2024.09.04
안드로이드의 논리적인 단위  (0) 2024.08.29
728x90

Naming convention을 지켜야하는 이유

  • resource의 쉬운 조회
  • 논지적이고 예측 가능한 이름
  • 자원의 깔끔한 순서
  • resource가 typed 됨

기본 원칙

모든 resouce의 name은 다음의 단순한 convention을 따릅니다

<WHAT>_<WHERE>_<DESCRIPTION>_<SIZE>

<WHAT>

resoucre가 실제로 나타내는 내용을 나타내며, 종종 Andriod view class의 standard 입니다. 리소스 종류당 제한된 옵션입니다.

(eg. MainActivity → activity)

<WHERE>

앱에서 논리적으로 어디에 속하는지 설명합니다. 여러 화면에서 사용되는 리소스를 사용하며, 다른 모든 화면은 해당 화면에 있는 android view 하위 class의 사용자 정의 부분을 사용합니다.

(e.g, MainActivity → ArticleDetailFragment → all main articledetail

<DESCRIPTION>

한 화면에서 여러 요소를 구분할 수 있습니다.

(e.g. title)

<SIZE> (optional)

정확한 크기 또는 크기 버킷입니다. drawable 및 dimensions에 선택적으로 사용됩니다

(e.g 240dp small)

Advantages

  1. 화면별 리소스 정렬
  2. 이 부분은 리소스가 어떤 화면에 속하는지를 설명합니다. 따라서 특정 화면에 대한 모든 ID, drawables, dimensions 등을 쉽게 찾을 수 있습니다.
  3. 강력한 타입의 리소스 IDWHAT findViewById()
  4. 리소스 ID의 경우, xml 요소에 속하는 클래스 이름을 설명합니다. 이로 인해 findViewById() 를 호출할 때 어떤 타입으로 캐스틩해야 하는지 쉽게 알 수 있습니다.
  5. 더 나은 리소스 정리
  6. 파일 브라우저나 프로젝트 탐색기는 일반적으로 파일을 알파벳 순으로 정렬합니다. 이는 layout과 drawable이 각각(activity, fragment 등)과 접두사에 따라 그룹화된다는 것을 의미합니다. 이제 간단한 Android Studio 플러그인이나 기능을 통해 이러한 resource를 각자의 폴더에 있는 것처럼 표시할 수 있습니다. what findViewById()
  7. 더 효율적인 자동 완성 리소스 이름이 훨씬 더 예측 가능해지기 때문에, IDE의 자동 완성을 사용하는 것이 더욱 쉬워집니다. 보통 WHAT이나 WHERE를 입력하는 것만으로 자동 완성을 한정된 옵션으로 좁히기에 충분합니다.
  8. 이름 충돌 방지
  9. 다른 화면에서 유사한 리소스는 다른 이름을 가지고 있습니다. 고정된 명명 naming scheme은 naming 충돌을 방지할 수 있습니다. all WHERE
  10. 깔끔한 리소스 이름
  11. 전반적으로 모든 리소스가 더 논리적으로 이름이 지정되어, 더 깔끔한 Android 프로젝트를 유지할 수 있습니다.
  12. 도구 지원
  13. 이 naming scheme은 Android Studio에서 쉽게 지원할 수 있으며, 예를 들어 다음과 같은 기능들을 제공할 수 있습니다. 이러한 이름을 강제하는 lint 규칙, 이름이나 리팩토링 지원, 프로젝트 보기에서 더 나은 리소스 시각화,… WHAT WHERE

Layouts

레이아웃은 일반적으로 화면당 몇 개의 레이아웃만 존재하기 때문에 상대적으로 간단합니다. 따라서 이 규칙을 다음과 같이 간소화할 수 있습니다:

<WHAT>_<WHERE>.xml

Prefix Usage

activity contentview for activity
fragment view for a fragment
view inflated by a custom view
item layout used in list/recycler/gridview
layout layout reused using the include tag

Exmalples:

  • activity_main: content view of the MainActivity
  • fragment_articledetail: view for the ArticleDetailFragment
  • view_menu: layout inflated by custom view class MenuView
  • item_article: list item in ArticleRecyclerView
  • layout_actionbar_backbutton: layout for an actionbar with a backbutton (too simple to be a customview)

Strings

Strings에 대한 부분은 관련이 없습니다. 따라서 문자열이 사용될 위치를 나타내기 위해 WHERE를 사용합니다: <WHAT> <WHERE>

<WHERE>_<DESCRIPTION>

또는 string이 앱에 전체적으로 사용된다면 all 을 사용합니다

all_<DESCRIPTION>

Examples:

  • articledetail_title: title of ArticleDetailFragment
  • feedback_explanation: feedback explanation in FeedbackFragment
  • feedback_namehint: hint of name field in FeedbackFragment
  • all_done: generic “done” string

<WHERE> 모든 resources에 대해 같은 view에서 명확하게 같다.

Drawables

드로어블(Drawables)에 대한 부분은 중요하지 않으므로, drawable이 사용될 위치를 나타내기 위해 <WHAT> <WHERE> 형식을 사용하거나 생략할 수 있습니다.

<WHERE><DESCRIPTION><SIZE>

또는 drawable이 모든 앱 내에서 재사용된다면 all을 사용합니다

all_<DESCRIPTION>_<SIZE>

그리고 추가적으로 실제 size를 넣을 수 있습니다. small 24dp

Examples:

  • articledetail_placeholder: placeholder in ArticleDetailFragment
  • all_infoicon: generic info icon
  • all_infoicon_large: large version of generic info icon
  • all_infoicon_24dp: 24dp version of generic info icon

IDs

ID의 경우, <WHAT>은 해당 ID가 속한 XML 요소의 클래스 이름을 나타냅니다. 그 다음으로는 ID가 위치한 화면을 나타내며, 동일한 화면 내에서 유사한 요소를 구분하기 위한 선택적 설명이 뒤따릅니다.

<WHAT><WHERE><DESCRIPTION>

Examples:

  • tablayout_main -> TabLayout in MainActivity
  • imageview_menu_profile -> profile image in custom MenuView
  • textview_articledetail_title -> title TextView in ArticleDetailFragment

Dimensions

앱은 제한된 차원의 세트만 정의하고, 이를 지속적으로 재사용해야 합니다. 이로 인해 대부분의 차원은 기본적으로 all로 지정됩니다.

보통은 아래형식으로 사용합니다.

<WHAT><WHERE><DESCRIPTION>_<SIZE>

또는 다음과 같이 사용되기도 합니다.

<WHAT>all<DESCRIPTION>_<SIZE>

<WHAT>의 경우는 아래의 종류 중에서 하나를 사용하는 경우가 많다.

Prefix Usage

width width in dp
height height in dp
size if width == height
margin margin in dp
padding padding in dp
elevation elevation in dp
keyline absolute keyline measured from view edge in dp
textsize size of text in sp

이 목록에는 가장 많이 사용되는 <WHAT>만 포함되어 있습니다. 회전(rotation), 스케일(scale)과 같은 기타 차원 한정자는 보통 drawables 에서만 사용되며, 따라서 재사용 빈도가 낮습니다.

Examples:

  • height_toolbar: height of all toolbars
  • keyline_listtext: listitem text is aligned at this keyline
  • textsize_medium: medium size of all text
  • size_menu_icon: size of icons in menu
  • height_menu_profileimage: height of profile image in menu

한계

  • 화면 이름의 고유성 필요: 인자 충돌을 피하기 위해, 클래스 이름은 고유해야 합니다. 예를 들어, "MainActivity"와 "MainFragment" 같은 이름을 동시에 사용할 수 없습니다.
  • 리팩토링 미지원: 클래스 이름을 변경해도 리소스 이름은 자동으로 변경되지 않습니다. 예를 들어 "MainActivity"를 "ContentActivity"로 변경해도 "activity_main"은 "activity_content"로 자동 변경되지 않습니다.
  • 모든 리소스 타입을 지원하지 않음: 제안된 방식은 일부 리소스 타입을 지원하지 않습니다. 특히 raw, assets, 테마, 스타일, 색상, 애니메이션 등은 일반화하기 어렵거나 덜 자주 사용되기 때문입니다.

reference

A successful XML naming convention · Jeroen Mols

728x90

'안드로이드' 카테고리의 다른 글

android CI 적용기  (1) 2024.12.23
desugaring이란?  (0) 2024.12.23
안드로이드의 논리적인 단위  (0) 2024.08.29
728x90

논리적인 단위

안드로이드 시스템은 기기의 크기를 ldpi, mpdi , hpdi, xhdpi, xxhdi, xxxhdpi 로 구분

여기서 dpi 는 dots per inch의 줄임말로 1인치 안에 있는 도트의 개수를 의미

크기 설명

ldpi 저밀도 화면 ~120dpi
mdpi 중밀도 화면 ~160dpi
hdpi 고밀도 화면 ~240dpi
xhdpi 초고밀도 화면 ~320dpi
xxhdpi 초초고밀도 화면 ~480dpi
xxxhdpi 초초초고밀도 화면 ~640dpi
  • dp(dip; density-independent pixels): 스크린의 물리적 밀도에 기반을 둔 단위
  • sp(sip: scale-independent pixels): dp와 유사하며 글꼴 크기에 적용
  • pt(points): 스크린 크기의 1/72를 1pt로 함
  • px : 픽셀
  • mm : 밀리미터
  • in : 인치

기기의 가로 세로 크기를 가져오는 법

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
    val windowMetrics: WindowMetrics = windowManager.currentWindowMetrics
    binding.textView.text = "width : ${windowMetrics.bounds.width()}, 
                            height : ${windowMetrics.bounds.height()}"
} else {
    val display = windowManager.defaultDisplay
    val displayMetrics = DisplayMetrics()
    display?.getRealMetrics(displayMetrics)
    binding.textView.text = "width : ${displayMetrics.widthPixels}, 
                            height : ${displayMetrics.heightPixels}"
}
728x90

'안드로이드' 카테고리의 다른 글

android CI 적용기  (1) 2024.12.23
desugaring이란?  (0) 2024.12.23
안드로이드 XML Naming Convention  (1) 2024.09.04

+ Recent posts