Jetpack Compose에서 사용할 수 있는 가볍고 독립적인 GitHub 잔디(contribution graph) UI 컴포넌트 라이브러리입니다.
Map<LocalDate, Int>만 넘기면 끝입니다.
GitHub의 잔디 그래프는 활동량을 한눈에 보여주는 훌륭한 시각화 방식입니다. 습관 추적, 공부 기록, 코딩 활동, 건강 데이터 등 매일의 활동을 기록하는 앱이라면 같은 패턴을 활용할 수 있지만, Compose에서 바로 쓸 수 있는 간단한 라이브러리가 없었습니다.
기존 접근 방식들은 Material3에 의존하거나, 앱 전용 모델에 묶여 있거나, 특정 프로젝트와 강하게 결합되어 있었습니다. compose-git-grass는 이 문제를 해결하기 위해 만들었습니다:
- UI 의존성 제로 - Compose Foundation(
BasicText,Box,Row)만 사용합니다. Material3가 필요 없습니다. - 입력 하나 -
Map<LocalDate, Int>면 충분합니다. 커스텀 모델도, 어댑터도, 보일러플레이트도 없습니다. - 완전한 커스터마이징 - 색상, 라벨, 크기, 임계값, 셀 클릭까지 모든 것을 파라미터로 설정할 수 있고, 기본값도 잘 잡혀 있습니다.
현재 최신 릴리즈는 1.2.0입니다.
// build.gradle.kts
dependencies {
implementation("io.github.ois0886:compose-git-grass:1.2.0")
}1.1.x에서 업그레이드:
selection파라미터가 기본값과 함께 추가되어 기존 호출은 그대로 동작합니다. 일요일 시작 그래프의 기본 요일 라벨은 이제 자동으로 일요일부터 정렬됩니다.GitGrassColors.border는 계속 deprecated 상태이며 2.0.0에서 제거될 예정입니다.
// 최소 사용 - 데이터만 넘기면 끝
GitGrass(
contributions = mapOf(
LocalDate.now() to 5,
LocalDate.now().minusDays(1) to 3,
)
)// 제어 가능한 날짜 선택
var selectedDate by remember { mutableStateOf<LocalDate?>(null) }
GitGrass(
contributions = data,
selection = selectedDate?.let { date ->
GitGrassSelection(date = date)
},
onCellClick = { date, _ ->
selectedDate = date
},
)// 다크모드 + 스트릭 + 셀 클릭 + 범례
GitGrass(
contributions = data,
colors = GitGrassDefaults.darkColors(),
showStreak = true,
showLegend = true,
onCellClick = { date, count ->
println("$date: ${count}회 기여")
},
)대부분의 1년 단위 그래프는 기본 설정만으로 충분히 가볍게 동작합니다. 여러 개의 그래프를 한 화면에 렌더링하거나 5년 이상의 긴 범위를 표시한다면 아래 원칙을 권장합니다:
- 별도
renderMode를 고를 필요는 없습니다. 내부에서 주 단위 lazy 렌더링과 Canvas 기반 셀 그리기를 자동으로 사용합니다. - 월 라벨과 잔디 셀은 하나의
LazyRow에서 함께 구성되므로 별도의 스크롤 동기화 비용이 없습니다. contributions,startDate,endDate, 로케일 라벨, 커스텀 색상 팔레트는remember나 ViewModel 상태로 안정적으로 전달하세요.- 필요 없는 보조 UI는
showMonthLabels,showStreak,showLegend를false로 꺼두면 해당 계산과 렌더링 비용을 줄일 수 있습니다. - 매 리컴포지션마다
LocalDate.now(),localizedMonthLabels(),GitGrassColors(...)를 새로 만들기보다 한 번 계산한 값을 재사용하세요. - 셀별 색상과 접근성 라벨은 입력이 바뀔 때만 내부 render data로 다시 계산되므로, 안정적인 입력을 전달할수록 리컴포지션 비용이 작아집니다.
val endDate = remember { LocalDate.now() }
val startDate = remember(endDate) { endDate.minusMonths(6) }
val monthLabels = remember { GitGrassDefaults.localizedMonthLabels() }
GitGrass(
contributions = contributions,
startDate = startDate,
endDate = endDate,
monthLabels = monthLabels,
showStreak = false,
)GitGrass는 선택 상태를 내부에서 변경하지 않습니다. 앱 상태를 GitGrassSelection으로 전달하고 클릭 콜백에서 갱신하세요.
var selectedDate by remember { mutableStateOf(LocalDate.now()) }
GitGrass(
contributions = data,
selection = GitGrassSelection(
date = selectedDate,
outlineColor = Color.Blue,
outlineWidth = 2.dp,
),
onCellClick = { date, count ->
selectedDate = date
println("$date: $count")
},
)선택 날짜가 표시 범위 밖이면 강조하지 않습니다. 선택 변경만으로 스크롤 위치가 바뀌지는 않으며, outlineColor = Color.Unspecified일 때는 현재 색상 스킴의 text 색상을 사용합니다.
원하는 색상과 레벨 수를 자유롭게 지정할 수 있습니다:
// 파란색 테마, 3단계
GitGrass(
contributions = data,
colors = GitGrassColors(
empty = Color(0xFFEEEEEE),
levels = listOf(
Color(0xFFBBDEFB),
Color(0xFF42A5F5),
Color(0xFF1565C0),
),
text = Color.Black,
),
levelOf = { count ->
when {
count <= 0 -> 0
count <= 5 -> 1
count <= 10 -> 2
else -> 3
}
},
)기본 제공 색상:
GitGrassDefaults.colors()- GitHub 라이트 테마GitGrassDefaults.darkColors()- GitHub 다크 테마
// 한국어 라벨 (수동)
GitGrass(
contributions = data,
weekLabels = listOf("월", "화", "수", "목", "금", "토", "일"),
monthLabels = listOf("", "1월", "2월", "3월", "4월", "5월", "6월",
"7월", "8월", "9월", "10월", "11월", "12월"),
showStreak = true,
streakMaxLabel = "최대 연속",
streakCurrentLabel = "현재 연속",
lessLabel = "적음",
moreLabel = "많음",
)// 디바이스 로케일 자동 감지 (1.0.0 신규)
GitGrass(
contributions = data,
monthLabels = GitGrassDefaults.localizedMonthLabels(),
weekLabels = GitGrassDefaults.localizedWeekLabels(),
)// 일요일 시작
GitGrass(
contributions = data,
weekStartDay = DayOfWeek.SUNDAY,
startDate = GitGrassDefaults.startDate(DayOfWeek.SUNDAY),
)영어 기본 라벨은 weekStartDay를 자동으로 따릅니다. 로컬라이즈된 라벨이 필요할 때만 GitGrassDefaults.localizedWeekLabels(DayOfWeek.SUNDAY)를 전달하면 됩니다.
GitGrass(
contributions = data,
onCellClick = { date, count -> /* 탭 */ },
onCellLongClick = { date, count -> /* 롱프레스 - 툴팁 표시 등 */ },
)GitGrass(
contributions = data,
cellSize = 16.dp,
cellSpacing = 4.dp,
cellCornerRadius = 4.dp,
labelFontSize = 12.sp,
)GitGrass(
contributions = data,
showYearLabel = false,
showWeekLabels = false,
showMonthLabels = true,
showStreak = true,
showLegend = true,
)| Parameter | Type | Default | Description |
|---|---|---|---|
contributions |
Map<LocalDate, Int> |
필수 | 날짜별 기여 횟수 |
startDate |
LocalDate |
~1년 전 (주 시작일 정렬) | 그래프 시작 날짜 |
endDate |
LocalDate |
오늘 | 그래프 끝 날짜 |
weekStartDay |
DayOfWeek |
MONDAY | 주 시작 요일 |
colors |
GitGrassColors |
GitHub 라이트 | 색상 스킴 |
monthLabels |
List<String> |
영어 | 월 이름 (인덱스 0 미사용) |
weekLabels |
List<String> |
주 시작일 기준 영어 | 요일 이름 |
cellSize |
Dp |
12.dp | 셀 크기 |
cellSpacing |
Dp |
3.dp | 셀 간격 |
cellCornerRadius |
Dp |
2.dp | 셀 모서리 반경 |
labelFontSize |
TextUnit |
10.sp | 라벨 폰트 크기 |
showYearLabel |
Boolean |
true | 연도 라벨 표시 |
showWeekLabels |
Boolean |
true | 요일 라벨 표시 |
showMonthLabels |
Boolean |
true | 월 라벨 표시 |
showStreak |
Boolean |
false | 스트릭 요약 표시 |
showLegend |
Boolean |
true | 색상 범례 표시 |
streakMaxLabel |
String |
"Max streak" | 최대 스트릭 라벨 |
streakCurrentLabel |
String |
"Current streak" | 현재 스트릭 라벨 |
lessLabel |
String |
"Less" | 범례 "적음" 라벨 |
moreLabel |
String |
"More" | 범례 "많음" 라벨 |
cellContentDescription |
(LocalDate, Int) -> String |
"$date: $count" |
셀 접근성 텍스트 (1.1.0) |
cellClickLabel |
(LocalDate) -> String |
"$date details" |
셀 클릭 접근성 라벨 (1.1.0) |
levelOf |
(Int) -> Int |
0/1-3/4-6/7-9/10+ | 기여 횟수 → 레벨 매핑 |
selection |
GitGrassSelection? |
null | 제어 가능한 선택 셀과 인셋 아웃라인 (1.2.0) |
onCellClick |
((LocalDate, Int) -> Unit)? |
null | 셀 탭 콜백 |
onCellLongClick |
((LocalDate, Int) -> Unit)? |
null | 셀 롱프레스 콜백 |
| Field | Description |
|---|---|
empty |
기여 없는 날의 색상 |
levels |
기여 레벨별 색상 리스트 (개수 자유) |
text |
라벨 텍스트 색상 |
border |
| Field | Default | Description |
|---|---|---|
date |
필수 | 선택해 강조할 날짜 |
outlineColor |
Color.Unspecified |
인셋 아웃라인 색상 (colors.text로 대체) |
outlineWidth |
2.dp | 인셋 아웃라인 두께 (셀 크기에 맞춰 안전하게 제한) |
잘못된 입력도 크래시 없이 안전하게 처리합니다:
- 날짜 역전 -
startDate > endDate이면 자동으로 교체 - 음수 값 - 0으로 클램프
- 누락된 날짜 - 기여 0회로 처리
- 짧은 라벨 리스트 - 부족한 항목은 빈 문자열로 대체
- 모든 셀에
contentDescription제공 (스크린 리더 지원) - 클릭 가능한 셀에
Role.Button및onClickLabel설정 - 선택된 셀에
selectedsemantics 제공 - 롱클릭 전용 셀에는 빈 클릭 액션 없이 롱클릭 semantics만 제공
- 그래프 루트에 "Contribution graph" semantics 적용
- 범례 셀에 "Level 0", "Level 1" 등 접근성 라벨 제공
cellContentDescription,cellClickLabel파라미터로 접근성 텍스트 커스터마이징 가능 (1.1.0)
// 한국어 접근성 텍스트 예시
GitGrass(
contributions = data,
cellContentDescription = { date, count -> "$date: ${count}건" },
cellClickLabel = { date -> "$date 상세보기" },
)- Pure Compose Foundation - Material3 의존 없이 Foundation의
BasicText만 사용하여, 어떤 디자인 시스템을 쓰는 프로젝트에서도 충돌 없이 동작합니다. - 유연한 색상 레벨 - 고정된
level1~level4대신levels: List<Color>를 사용합니다. 3단계든, 5단계든, 10단계든 자유롭게 지정할 수 있습니다. - 픽셀 단위 월 라벨 정렬 - 월 라벨이 그리드와 동일한
Arrangement.spacedBy+ 고정 너비 슬롯 구조를 사용하여, 어떤 셀 크기에서도 정확하게 정렬됩니다. - 자동 최적화 렌더링 - 긴 날짜 범위에서는 주 단위
LazyRow가 화면에 필요한 열만 구성하고, 셀 색상은 Canvas로 그려 반복 노드 비용을 줄입니다. - 단일 LazyRow - 월 라벨과 주별 Canvas를 같은 lazy item에서 구성해 스크롤 정합성을 구조적으로 보장합니다.
- 선택 렌더링 분리 - 선택 아웃라인은 render grid를 다시 만들지 않고 보이는 Canvas와 semantics만 갱신합니다.
- 순수 함수 & 테스트 - 그리드/스트릭/월 경계/초기 스크롤 계산이 순수 함수로 구현되어 있으며, 유닛 테스트와 Compose UI 테스트가 엣지 케이스와 인터랙션을 검증합니다.
- 접근성 - 모든 셀에 contentDescription, semantics 적용으로 스크린 리더를 지원합니다.
- 완전한 주 단위 자동 스크롤 - 첫 컴포지션에서 최신 날짜를 유지하면서 시작 월 라벨이 잘리지 않는 완전한 주 열로 스냅합니다.
- Min SDK 26 (Android 8.0)
- Jetpack Compose (Foundation)
Copyright 2026 Inseong
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.

