๊ฐœ๋ฐœ ์ผ์ง€ ๐Ÿ’ป/Nutri Capture

Nutri Capture ํ”„๋ก ํŠธ์—”๋“œ - requestScrollToItem()์„ ์ด์šฉํ•œ ๊น”๋”ํ•œ ์—ญ๋ฐฉํ–ฅ ๋ฌดํ•œ ์Šคํฌ๋กค

interfacer_han 2024. 10. 16. 09:36

#1 ์Šคํฌ๋กค ํ•จ์ˆ˜ ๋ณ€๊ฒฝ

#1-1 ๊ธฐ์กด ํ•จ์ˆ˜

LazyListState.scrollToItem() ๋ฐ LazyListState.scrollBy()๋Š”, ์‹œ์Šคํ…œ ์ƒ์˜ ์ œ์•ฝ์ด ์กด์žฌํ•œ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ํ™”๋ฉด์— ์†์„ ๋ถ™์ธ ์ฑ„๋กœ ์œ ์ง€ํ•˜๋ฉด ์Šคํฌ๋กค์ด ์•„์˜ˆ ์ž ๊ฒจ๋ฒ„๋ฆฌ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ์ด ์‹œ์Šคํ…œ ์ƒ์˜ ์ œ์•ฝ์„ ์šฐํšŒํ•˜๋ ค๊ณ  ์ •๋ง ๋งŽ์€ ์ฝ”๋“œ๋ฅผ ์‹œ๋„ํ•ด๋ณด์•˜์ง€๋งŒ, ์ œ๋Œ€๋กœ ์ž‘๋™ํ•˜๊ธฐ ์•Š์•˜๊ณ  ์ž‘๋™ํ•˜๋”๋ผ๋„ ์•ฑ์ด ๊ต‰์žฅํžˆ ์กฐ์žกํ•ด๋ณด์ด๋Š” ๋ชจ์–‘์ƒˆ์˜€๋‹ค.
 

#1-2 LazyListState.requestScrollToItem()

 

LazyListState  |  Android Developers

 

developer.android.com

๊ทธ๋Ÿฌ๋‹ค ์ฐพ์€ ํ•จ์ˆ˜๊ฐ€ LazyListState.requestScrollToItem()๋‹ค. ๋‹ค์Œ Recomposition ๋•Œ ์Šคํฌ๋กค์ด ์œ„์น˜ํ•ด์•ผํ•˜๋Š” ๋ถ€๋ถ„์„ ์ง€์ •ํ•˜๋Š” ํ•จ์ˆ˜๋กœ, LazyListState.scrollToItem() ๋ฐ LazyListState.scrollBy()์ฒ˜๋Ÿผ ๋ช…๋ น์ ์œผ๋กœ ์Šคํฌ๋กค์„ ์กฐ์ž‘ํ•˜๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ ์„ ์–ธ์ ์œผ๋กœ Compose Runtime์—๊ฒŒ ์Šคํฌ๋กค ์œ„์น˜๋ฅผ ๋ถ€ํƒํ•˜๋Š” ํ•จ์ˆ˜๋‹ค. '์Šคํฌ๋กค ์ง์ ‘ ์กฐ์ž‘ ๋ช…๋ น'์€ ์‚ฌ์šฉ์ž์˜ ํ„ฐ์น˜ ์ด๋ฒคํŠธ์— ์˜ํ•ด ์šฐ์„  ์ˆœ์œ„๊ฐ€ ๋ฐ€๋ ค Blocking๋  ์ˆ˜ ์žˆ์ง€๋งŒ, Recomposition ์‹œ์˜ ๊ฒฐ์ •๋˜๋Š” ์Šคํฌ๋กค ์œ„์น˜๋Š” ์‚ฌ์šฉ์ž์˜ ํ„ฐ์น˜ ์ด๋ฒคํŠธ๊ณผ ๋…๋ฆฝ์ ์ด๋‹ค. ๋”ฐ๋ผ์„œ ๋‚ด๊ฐ€ ๊ตฌํ˜„ํ•˜๊ณ ์ž ํ–ˆ๋˜ ๋งค์šฐ ๊น”๋”ํ•˜๊ณ  ๋งค๋„๋Ÿฌ์šด ์—ญ๋ฐฉํ–ฅ ๋ฌดํ•œ ์Šคํฌ๋กค์ด ๊ตฌํ˜„๋œ๋‹ค. ๋˜, LazyListState.requestScrollToItem()๋Š” LazyListState.scrollToItem() ๋ฐ LazyListState.scrollBy()์ฒ˜๋Ÿผ ๋น„๋™๊ธฐ ์ฝ”๋“œ๊ฐ€ ์•„๋‹ˆ๋ผ ๋™๊ธฐ ์ฝ”๋“œ๋‹ค (์ฆ‰, suspend ํ‚ค์›Œ๋“œ๊ฐ€ ๋ถ™์ง€ ์•Š๋Š”๋‹ค). requestScrollToItem()์€ ๋‹ค์Œ Recomposition ๋•Œ์˜ ์ž‘์—…์„ ์š”์ฒญ๋งŒํ•˜๋Š” ํ•จ์ˆ˜์ด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.
 
... ์ฒ˜์Œ๋ถ€ํ„ฐ ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ๊ผผ๊ผผํžˆ ์ฝ์„ ๊ฑธ ๊ทธ๋žฌ๋‹ค. ์ด๋ฏธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ์กด์žฌํ•˜๋Š” ๊ธฐ๋Šฅ์„ ํ˜ผ์ž์„œ ๊ตฌํ˜„ํ•ด๋ณด๋ ค๊ณ  ๋ฉฐ์น ๊ฐ„ ์ฉ”์ฉ”๋งจ ๊ผด์ด ์•„๋‹Œ๊ฐ€. ์•„๋ฌด๋ž˜๋„ ์˜์–ด๋ผ์„œ ๊ทธ๋Ÿฐ ๊ฒƒ๋„ ๊ฐ™๋‹ค. ๊ทธ๋Ÿผ์—๋„ ๋ฒˆ์—ญ๊ธฐ๊นŒ์ง€ ๋™์›ํ•ด์„œ ์ฝ์„ ๊ฐ€์น˜๋Š” ์ถฉ๋ถ„ํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ•œ๋‹ค. ๋ช‡ ์‹œ๊ฐ„์˜ ๊ณต์‹ ๋ฌธ์„œ ์ •๋…์ด ๋ช‡์‹ญ ์‹œ๊ฐ„์„ ์•„๊ปด์ค€๋‹ค. 
 

#2 ์ฝ”๋“œ

#2-1 NutrientScreenEvent.ScrollToItem ์ด๋ฒคํŠธ ์ด๋ฆ„ ๋ณ€๊ฒฝ ๋ฐ ๊ตฌํ˜„ ๋‚ด์šฉ ์ˆ˜์ •

...

@Composable
fun NutrientScreen(
    ...
) {
    LaunchedEffect(key1 = true) {
        // State ์ดˆ๊ธฐํ™”
        ...

        // ViewModel๋กœ๋ถ€ํ„ฐ ๋ฐ›์€ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ
        viewModel.nutrientScreenEventFlow.collectLatest { event ->
            when (event) {
                is NutrientScreenEvent.ShowSnackbar -> {
                    ...
                }

                is NutrientScreenEvent.RequestScrollToItem -> {
                    Log.i("interfacer_han", "(์ด๋ฒคํŠธ ScrollToItem) ์‹œ์ž‘ (์•„์ดํ…œ ๊ฐฏ์ˆ˜: ${viewModel.nutrientScreenState.value.dailyMeals.size})")
                    listState.requestScrollToItem(event.index, listState.firstVisibleItemScrollOffset)
                    Log.i("interfacer_han", "(์ด๋ฒคํŠธ ScrollToItem) ๋ (์•„์ดํ…œ ๊ฐฏ์ˆ˜: ${viewModel.nutrientScreenState.value.dailyMeals.size})")
                }
            }
        }
    }

    LaunchedEffect(key1 = viewModel.isInitialized.value) {
        ...
    }

    LazyColumn(
        ...
    ) {
        ...
    }
}

NutrientScreenEvent.ScrollToItem๋ฅผ NutrientScreenEvent.RequestScrollToItem๋กœ ๋ฐ”๊พผ๋‹ค. ๊ตฌํ˜„ ๋‚ด์šฉ์— ์žˆ๋˜ listState.scrollToItem()์€ ์‚ญ์ œํ•˜๊ณ  requestScrollToItem()์„ ๋Œ€์‹  ์‚ฌ์šฉํ•œ๋‹ค.
 

public final fun requestScrollToItem(
    @IntRange(from = 0.toLong()) index: Int,
    scrollOffset: Int = 0
): Unit

requestScrollToItem()์˜ ์ฒซ๋ฒˆ์งธ ์ธ์ˆ˜๋Š” ํ™”๋ฉด์— ๋ณด์ผ ์ฒซ๋ฒˆ์งธ ์•„์ดํ…œ, ๋‘๋ฒˆ์งธ ์ธ์ˆ˜๋Š” scrollOffset์ด๋‹ค. scrollOffset์€ '์–ผ๋งˆ๋‚˜ ์Šคํฌ๋กค๋˜์—ˆ๋Š”์ง€์˜ ์ •๋„๊ฐ’'์ด๋‹ค. scrollOffset์ด ์‚ฌ์šฉ๋œ ์ฝ”๋“œ๋Š” ์ง๊ด€์ ์œผ๋กœ ์ดํ•ดํ•˜๊ธฐ ์–ด๋ ต๋‹ค. ๋”ฐ๋ผ์„œ ๊ฐ„๋‹จํ•˜๊ฒŒ ๊ทธ ๊ตฌ์กฐ๋ฅผ ์•„๋ž˜์— ๋‚จ๊ฒจ๋†“์œผ๋ ค๊ณ  ํ•œ๋‹ค.
 

#2-2 scrollOffset ์œ ๋ฌด์˜ ์ฐจ์ด

๋จผ์ €, requestScrollToItem()์—์„œ scrollOffset์ด 0์ธ ๊ฒฝ์šฐ์˜ ๋„์‹๋„๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค.
 

์›๋ž˜ index์˜ ์ตœ์†Ÿ๊ฐ’์€ 0์ด์ง€๋งŒ, ์ง๊ด€์ ์ธ ์ดํ•ด๋ฅผ ์œ„ํ•ด ๋์—†์ด ๋‚ฎ์•„์งˆ ์ˆ˜ ์žˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•œ๋‹ค.  1 ์—์„œ  2 ์˜ ์ƒํƒœ๊ฐ€ ๋  ์ •๋„๋กœ ์Šคํฌ๋กค์„ ์ง„ํ–‰ํ–ˆ๋‹ค๊ณ  ํ•ด๋ณด์ž. ์ด๋Ÿฌ๋ฉด, ์ฒซ๋ฒˆ์งธ ์•„์ดํ…œ์ด ํ™”๋ฉด์— ๋…ธ์ถœ๋œ ์ƒํƒœ๊ธฐ ๋•Œ๋ฌธ์— ๋ฌดํ•œ ์Šคํฌ๋กค ๋กœ์ง์ด ์ž‘๋™ํ•ด  3 ์˜ ์ƒํƒœ๊ฐ€ ๋œ๋‹ค. ํ™”๋ฉด์ด  2 ์—์„œ ๊ฐ‘์ž๊ธฐ  3 ์ด ๋˜๋ฉด ๋ญ”๊ฐ€ ๋š๋š ๋Š์–ด์ง€๋Š” ๋А๋‚Œ์ด ๋‚  ๊ฒƒ์ด๋‹ค. 
 
๋ฐ˜๋ฉด,  requestScrollToItem()์—์„œ scrollOffset์ด 0์ด ์•„๋‹Œ ๊ฒฝ์šฐ๋„ ์žˆ๋‹ค. #2-1์—์„œ ์‚ฌ์šฉ๋œ scrollOffset์€ LazyListState.firstVisibleItemScrollOffset๋กœ, ํ™”๋ฉด์— ๋ณด์ด๋Š” ์ฒซ๋ฒˆ์งธ ์•„์ดํ…œ์ด ์–ผ๋งˆ๋‚˜ ์Šคํฌ๋กค๋˜์—ˆ๋Š”์ง€๋ฅผ ์˜๋ฏธํ•˜๋Š” ๊ฐ’์ด๋‹ค. ์ด ๊ฒฝ์šฐ์˜ ๋„์‹๋„๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค.
 

 2 ์—์„œ  3 ์œผ๋กœ ๊ฐˆ ๋•Œ, index 0์ด ํ™”๋ฉด์˜ ๋งจ ์œ„์—์„œ ์Šคํฌ๋กค๋œ ์ •๋„๊ฐ’๋งŒํผ์„ ์ถ”๊ฐ€๋กœ ๋”ํ•ด์ฃผ๋ฉด  3 ์ด ์•„๋‹ˆ๋ผ  3' ์ด ๋œ๋‹ค. ์ด๋Ÿฌ๋ฉด ์‚ฌ์šฉ์ž ์ž…์žฅ์—์„œ ์•„์ดํ…œ์ด ์ถ”๊ฐ€๋˜์—ˆ์Œ์—๋„ ๋ˆˆ์— ๋ณด์ด๋Š” ํ™”๋ฉด์€ ๋ณ€ํ•จ์—†์ด ์—ฐ์†๋œ ๊ฒƒ์ฒ˜๋Ÿผ ๋ณด์ธ๋‹ค.
 

#2-3 Log.i( ... ) ์ „๋ถ€ ์‚ญ์ œ

ํ”„๋กœ์ ํŠธ์— ์žˆ๋˜ Log.i( ... )๋ฅผ ์ „๋ถ€ ์‚ญ์ œํ•œ๋‹ค. ๋‚ด๊ฐ€ ์›ํ•˜๋Š” ์ˆ˜์ค€์˜ ์—ญ๋ฐฉํ–ฅ ๋ฌดํ•œ ์Šคํฌ๋กค ๊ตฌํ˜„์„ ์œ„ํ•ด์„ , ํ•จ์ˆ˜ ๊ฐ„ ์‹คํ–‰ ์ˆœ์„œ๋ฅผ ์ธ์ง€ํ•˜๊ณ  ์žˆ์–ด์•ผ ํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๋‹ค. ๊ทธ๋ž˜์„œ ์ด์ „ ๊ฒŒ์‹œ๊ธ€์—์„œ ๋‚จ๊ฒจ๋‘์—ˆ๋˜ ๋กœ๊ทธ ์ฝ”๋“œ์ธ๋ฐ, ๋ณธ ๊ฒŒ์‹œ๊ธ€์„ ํ†ตํ•ด ์—ญ๋ฐฉํ–ฅ ๋ฌดํ•œ ์Šคํฌ๋กค์€ ์•„์ฃผ ๊น”๋”ํ•˜๊ฒŒ ํ•ด๊ฒฐ๋˜์—ˆ์œผ๋‹ˆ ์ด์ œ ํ•„์š”๊ฐ€ ์—†๋‹ค. ๋‹น์—ฐํ•˜๊ฒ ์ง€๋งŒ #2-1์˜ ์ฝ”๋“œ์— ์žˆ๋Š” Log.i( ... )๋„ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ์‚ญ์ œํ•œ๋‹ค.
 

#2-4 NutrientViewModelEvent.InitializeState ์ด๋ฒคํŠธ ๊ตฌํ˜„ ์ˆ˜์ •

...

class NutrientViewModel : ViewModel() {
    // (1) ํ™”๋ฉด ํ‘œ์‹œ์šฉ State
    ...

    // (2) ViewModel์šฉ ๋‚ด๋ถ€ ๋ณ€์ˆ˜
    ...

    // (3) View์—์„œ ๋ฐ›์•„ ์ฒ˜๋ฆฌํ•  ์ด๋ฒคํŠธ
    ...

    // (4) View๋กœ๋ถ€ํ„ฐ ๋ฐ›์€ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ
    fun onEvent(event: NutrientViewModelEvent) {
        when (event) {
            is NutrientViewModelEvent.InitializeState -> {
                var dateToInsert = LocalDate.now()
                repeat(20) {
                    _nutrientScreenState.value.dailyMeals.add(
                        DailyMeal(
                            date = dateToInsert,
                            meals = SnapshotStateList()
                        )
                    )
                    dateToInsert = dateToInsert.plusDays(1)
                }

                viewModelScope.launch {
                    delay(100)
                    _isInitialized.value = true
                }
            }

            is NutrientViewModelEvent.LoadMoreItemsAfterLastDate -> {
                ...
            }

            is NutrientViewModelEvent.LoadMoreItemsBeforeFirstDate -> {
                ...
            }
        }
    }
}

_isInitialized.value = true ๊ตฌ๋ฌธ์„ ์ฝ”๋ฃจํ‹ด ์˜์—ญ์— ๋„ฃ๊ณ  0.1์ดˆ ์žˆ๋‹ค๊ฐ€ ์‹คํ–‰๋˜๊ฒŒ ์ˆ˜์ •ํ–ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ์ˆ˜์ •ํ•˜์ง€ ์•Š์œผ๋ฉด, ์•ฑ์„ ๋งจ ์ฒ˜์Œ ์‹คํ–‰์‹œ์ผฐ์„ ๋•Œ ํ™”๋ฉด์—์„œ ์˜ค๋Š˜ ๋‚ ์งœ๊ฐ€ ์•„๋‹ˆ๋ผ ๊ทธ ์ „ ๋‚ ์˜ ๋‚ ์งœ๊ฐ€ ์ฒ˜์Œ์œผ๋กœ ํ‘œ์‹œ๋œ๋‹ค. ์Šคํฌ๋กค์— ๊ด€๋ จํ•œ ์—๋Ÿฌ๋กœ, requestScrollToItem์ด ์ œ๋Œ€๋กœ ์ž‘๋™ํ•˜์ง€ ์•Š์•˜๋‹ค๋Š” ๋ง์ด๋‹ค. ์ถ”์ธก์ปจ๋Œ€ LazyColumn์— ๋“ค์–ด๊ฐˆ ์•„์ดํ…œ ๋ฆฌ์ŠคํŠธ์— ์•„์ดํ…œ์„ ์ถ”๊ฐ€ํ•จ๊ณผ ๋™์‹œ์— _isInitialized.value์˜ ๊ฐ’์„ ๋ณ€๊ฒฝํ•ด์„œ, ๋‘˜์˜ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์ด ํ•œ๊บผ๋ฒˆ์— ๋‹ค์Œ ๋ฒˆ Recomposition์— ๋ฐ˜์˜๋˜์–ด ์ผ์–ด๋‚˜๋Š” ์—๋Ÿฌ๋กœ ๋ณด์ธ๋‹ค. ํ•ด๊ฒฐ์„ ์œ„ํ•ด ๋”ฐ๋ผ์„œ ์•ฝ๊ฐ„์˜ ๋”œ๋ ˆ์ด๋ฅผ ๋‘์—ˆ๋‹ค. delay(100)์ด ์•„๋‹ˆ๋ผ delay(1)๋กœ ๋‘์–ด๋„ ์ž˜ ์ž‘๋™ํ•˜์ง€๋งŒ ์ผ๋‹จ์€ delay(100)๋ผ๊ณ  ๋‘์—ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ด ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋ฉฐ #3์— ์žˆ๋Š” ๊ฐœ์„  ๋ฐฉํ–ฅ์„ฑ์ด ๋– ์˜ฌ๋ž๋‹ค.
 

#3 ๊ฐœ์„  ๋ฐฉ์•ˆ

๋ฐ”๋กœ, ViewModel์—์„œ LazyColumn์ด ๋ณด์œ ํ•  ์•„์ดํ…œ ๋ฆฌ์ŠคํŠธ๋ฅผ ์กฐ์ž‘ํ•œ ์ดํ›„์— ์Šคํฌ๋กค ๊ด€๋ จ ์ด๋ฒคํŠธ๋ฅผ View์— ๋ณด๋‚ด์ฃผ๋Š” ๊ตฌ์กฐ๊ฐ€ ๊ทธ๋ ‡๋‹ค. Model์ด '์žฌ๋ฃŒ'๊ณ  View๊ฐ€ '์š”๋ฆฌ'๋ผ๋ฉด, View Model์€ 'ํ•„์š”ํ•œ ์žฌ๋ฃŒ๊ฐ€ ์˜ฌ๋ผ๊ฐ€์žˆ๋Š” ๋„๋งˆ'๋‹ค. ViewModel์—์„œ๋Š” ์•„์ดํ…œ ๋ฆฌ์ŠคํŠธ์˜ ๊ด€๋ฆฌ๋งŒ ๋‹ด๋‹นํ•˜๊ณ , ์Šคํฌ๋กค ๊ด€๋ จํ•œ ๋™์ž‘์€ View์—๊ฒŒ ์˜จ์ „ํžˆ ๋„˜๊ฒจ์ฃผ์–ด์•ผ ํ•œ๋‹ค๊ณ  ๋ณธ๋‹ค. #2-4๋งŒ ๋ด๋„ ๋‹น์žฅ์˜ ์—๋Ÿฌ๋ฅผ ๊ณ ์น˜๊ธฐ ์œ„ํ•œ ์˜๋ฏธ ์—†๋Š” delay(100)์ด ์‚ฌ์šฉ๋˜์—ˆ๋Š”๋ฐ, ์ด๋Ÿฐ ๋ฌธ์ œ๋Š” ์ž˜๋ชป๋œ ๊ตฌ์กฐ์—์„œ ๋‚˜์˜ค๋Š” ๊ฒƒ์œผ๋กœ ์ƒ๊ฐ๋œ๋‹ค. ๋‹น์žฅ ๋‹ค์Œ ์ž‘์—…์œผ๋กœ ์ด ๊ตฌ์กฐ์˜ ๊ฐœ์„ ์„ ํ•  ์ˆ˜๋„ ์žˆ์ง€๋งŒ, ํฌ๊ฒŒ ๊ธ‰ํ•œ ๋ถ€๋ถ„์€ ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์— ๋‹ค๋ฅธ ์ž‘์—…์„ ๋จผ์ € ์ง„ํ–‰ํ•  ์ˆ˜๋„ ์žˆ๊ฒ ๋‹ค.
 

#4 ์š”์•ฝ

LazyListState์— ๋‚ด๊ฐ€ ์›ํ•˜๋Š” ํ•จ์ˆ˜๊ฐ€ ์žˆ์—ˆ๋‹ค. ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ์ž˜ ์ฝ๋Š” ์Šต๊ด€์„ ๋“ค์ด์ž. ๋˜, ์Šคํฌ๋กค ๊ด€๋ จํ•œ ๋™์ž‘์„ View๊ฐ€ ์˜จ์ „ํžˆ 100% ๋‹ด๋‹นํ•˜๊ฒŒ๋” ๋งŒ๋“ค์–ด์•ผ ํ•  ๊ฐœ์„  ๋ฐฉํ–ฅ์ด ๋ณด์ธ๋‹ค.
 

#5 ์™„์„ฑ๋œ ์•ฑ

#5-1 ์ž‘๋™ ์˜์ƒ

 

#5-2 ์ด ๊ฒŒ์‹œ๊ธ€ ์‹œ์ ์˜ Commit

 

GitHub - Kanmanemone/nutri-capture-new

Contribute to Kanmanemone/nutri-capture-new development by creating an account on GitHub.

github.com

 

#5-3 ๋ณธ ํ”„๋กœ์ ํŠธ์˜ ๊ฐ€์žฅ ์ตœ์‹  Commit

 

GitHub - Kanmanemone/nutri-capture-new

Contribute to Kanmanemone/nutri-capture-new development by creating an account on GitHub.

github.com