해당 페이지는 위 페이지의 7번 튜토리얼에 대한 번역 및 구현예제입니다.
basically, this page follows the implementation and translation of the tutorial code above link.
[추가]리비전 내용 : 아래 내용을 참고하여 보충합니다.
https://developer.android.com/jetpack/compose/state
https://kotlinworld.com/176?category=973278
0. 본인이 이해하는 Compose를 요약한다면..
Function에서 Stateless한 UI Function과 State를 분리하여, Unidirectional Data flow(단방향 데이터 흐름)로 State와 Event를 교환하는 widget들로 선언형 패러다임을 따르게 만든 UI framework라고 생각한다.
1. motivation of using "Compose"
1> imperative programming paradigm을 따르는 기존의 방식은 xml <-> java/kotlin 의 binding에 의한 Stateful 구조임.
2> 예를 들어, textView에 대한 UI를 control 하는 예제를 생각해보면,
TextView속성을 지는 our_new_title이 .xml에 description 되어 있으면, 이를 사용하기 위해서 R.id.our_new_title를 findViewById를 통해 View Binding하는 과정을 거쳐 setter를 호출하여 값을 변경한다.
findViewById<TextView>(R.id.our_new_title).text = "this is revision part"
위의 코드가 실행되면, our_new_title이라는 변수명을 가지는 TextView는 text property로 this is revision part라는 문자열을 직접 가지고 있고, 이는 TextView 내부적으로 직접 text property를 가지고 유지하는 역할까지 수행하고 있으므로, Stateful하다. 이러한 방식의 뷰 조작 방식은 (기존까지 잘 사용되어져 왔으나) detailed controlling 하지 않으면, 의도치 않은 동작을 유발할 가능성이 높다. View가 복잡해질 수록 이러한 side effect를 사용한 코드를 통한 뷰 컨트롤은 오류 가능성이 매우 높아 지게 되는 것이다.
이를 해결하기 위해 Compose 가 등장하였고, Compose는 declarative programming paradigm을 따르는 UI drawing Framework로 정의된다.
Compose는 View에 대한 사항들을 Widget화 하여 관리할 수 있으며, 이러한 Widget들을 위의 선언형 패러다임을 따르는 것과는 다르게 side-effect를 가지는 변수들을 parameterize하여 Stateless하게 변환할 수 있다.
아래는 Stateless와 Stateful의 상태를 표현한 간단한 코드이다.
//StateLess Widget
@Compose
fun NewTitle(name : String){
Row{
Text(name)
}
}
//Stateful Widget
@Compose
fun NewTitle(name : String){
Row{
Text("this text hold a value of this text so it is stateful state")
}
}
3> 장점 : 이러한 방식으로 Stateless한 방식을 취하게 되면 기존에 사용되고 있던 MVVM 혹은 MVI 구조를 더 나은 방식으로 사용할 수 있다. VM에서 state를 구성하고, stateless한 Compose widget에 VM의 state를 전달 받아 View를 drawing하면 side-effect를 최대한 줄인 유지보수가 용이한 스타일로 코드를 설계할 수 있다.
2. About "State"
State in an app is any value that can change over time. This is a very broad definition and encompasses everything from a Room database to a variable on a class
"State"란, App상에서 시간에 따라 변할 수 있는 그 어떤 value라도 지칭할 수 있는 definition이다. 이 definition은 a Room database로부터 class의 variable까지 모든 것을 encompass한다.
All Android apps display state to the user. A few examples of state in Android apps:
- A Snackbar that shows when a network connection can't be established.
- A blog post and associated comments.
- Ripple animations on buttons that play when a user clicks them.
- Stickers that a user can draw on top of an image.
Jetpack Compose helps you be explicit about where and how you store and use state in an Android app. This guide focuses on the connection between state and composables, and on the APIs that Jetpack Compose offers to work with state more easily.
모든 App은 사용자에게 State가 display된다.
예를 들어,
- 네트워크 연결을 설정할 수 없을 때 표시되는 스낵바
- 블로그 게시물 및 관련 댓글
- 사용자가 클릭하면 버튼에서 재생되는 물결 애니메이션
- 사용자가 이미지 위에 그릴 수 있는 스티커
Compose는 어디서 어떻게 이러한 state를 사용하고 저장할 것인지를 explicit하게 나타낼 수 있도록 해준다.
구현을 하다보면, state와 composable의 connection에 대해 초점을 맞추고, state를 어떻게 easily offer할 것인지에 대해 알게 될 것이다.
2. Getting set up
패키지의 start code로 출발하여 finish code가 되도록 하나씩 구현해봅니다.
- examples – Example Activities for exploring the concepts of unidirectional data flow. You will not need to edit this package. 비방향성 데이터 플로우 개념을 가진 액티비티 샘플입니다.
- ui – Contains themes auto-generated by Android Studio when starting a new compose project. You will not need to edit this package. 컴포즈 프로젝트를 사용하였을때 기본적으로 생성되는 패키지입니다.
- util – Contains helper code for the project. You will not need to edit this package. util 패키지는 프로젝트의 코드헬퍼 클래스들을 모아놓습니다.
- todo – The package containing the code for the Todo screen we are building. You will be making modifications to this package. 코드랩 학습자는 이 패키지를 수정합니다.
Provided files in todo package
- Data.kt – Data structures used to represent a TodoItem : a TodoItem를 표현하기 위해 사용되는 데이터 구조에 대한 기술을 합니다.
- TodoComponents.kt – Reusable composables that you will use to build the Todo screen. You will not need to edit this file. : Todo screen를 만들기 위해 사용하는 재사용성 높은 컴포저블에 대한 파일입니다. 이를 수정할 것입니다.
Files you will edit in todo package
- TodoActivity.kt – Android Activity that will use Compose to draw a Todo screen after you're done with this codelab. 학습자가 만들 콤포즈를 drawing할 액티비티입니다.
- TodoViewModel.kt – A ViewModel that you will integrate with Compose to build the Todo screen. You will connect it to Compose and extend it to add more features as you complete this codelab. 뷰모델입니다.
- TodoScreen.kt – Compose implementation of a Todo screen that you will build during this codelab. 컴포즈는 TodoScreen을 임플리멘트합니다..
3. Understanding Unidirectional Data Flow
Unidirectional Data flow 유니디렉셔널 데이터 흐름(단 방향흐름)에 대한 이해.
- Event – An event is generated by the user or another part of the program
- Update State – An event handler changes the state that is used by the UI
- Display State – The UI is updated to display the new state
단방향흐름은, 이벤트가 발생하면, 이벤트 핸들러가 State를 변경시키며, 그 new State에 의해 Ui가 update되는 방식입니다.
기존의 xml binding을 이용한 UI 업데이트 방식은 unstructured state 가 저장되는 방식입니다. 이는 복잡한 구조의 ui update 구현시 난이도가 상승하는 문제를 가져옵니다. 그러한 문제는 부분 상태를 업데이트하거나, 부분UI를 업데이트하거나 테스트를 하게 되는 대부분의 경우를 포함합니다.
이 문제를 해결하고자 AAC는 ViewModel과 LiveData를 포함하였습니다. LiveData에의해 Observing 되는 값을 Layout이 반영하는 구조는 위의 단방향흐름의 맥락과 동일합니다. 이를 다시 생각하면, 아래와 같은 Event와 State에 대한 반영이라 할 수 있습니다.
4. Compose and ViewModels
3. 을 요약하자면 아래와 같습니다.
- State – any value that can change over time
- Event – notify a part of a program that something has happened
- Unidirectional data flow – a design where events flow up and state flows down
먼저, 살펴볼 파일은 아래와 같습니다.
- TodoScreen.kt – These composables interact directly with state and we will be editing this file as we explore state in compose.
- TodoComponents.kt – These composables define reusable bits of UI that we will use to build the TodoScreen. You do not need to edit these composables to complete this codelab.
위의 코드를 아래와 같은 기능이 되도록 만들 것이다
This composable displays an editable TODO list, but it doesn't have any state of its own. Remember, state is any value that can change – but none of the arguments to TodoScreen can be modified.
- items – an immutable list of items to display on the screen
- onAddItem – an event for when the user requests adding an item
- onRemoveItem – an event for when the user requests removing an item
"상태"라는 것은 변할수 있는 어떠한(Any) 값을 말한다. 그러나 TodoScreen에 전달되는 arguments중 어떠한것도 수정되어서는 안된다.
A stateless composable is a composable that cannot directly change any state.
Stateless는 Any State에 의해서도 직접적으로 변할 수 없는 불변성을 가진 composable이다. 이는 순수함수와 비슷한 느낌이다.
이 stateless로 display를 바꾸는 역할은 state hoisting을 통해 이루어진다.
State hoisting is the pattern of moving state up to make a component stateless. Stateless components are easier to test, tend to have fewer bugs, and open up more opportunities for reuse.
이에 대한 동작 플로우는 아래와 같다
- Event – when the user requests an item be added or removed TodoScreen calls onAddItem or onRemoveItem 이벤트가 발생하면 TodoScreen은 onAddItem나 onRemoveItem 메소드를 콜한다.
- Update state – the caller of TodoScreen can respond to these events by updating state. 상태를 업데이트를 통해 호출자는 이러한 이벤트들에 대한 응답을 할 수 있다.
- Display state – when the state is updated, TodoScreen will be called again with the new items and it can display them on screen. 상태가 업데이트되면, new items을 통해 다시 호출을 할 수 있는 상태가 된다.
호출자(The caller)는 어디서, 어떻게 상태를 hold하고 있을지를 이해해야할 책임을 가지고 있고, 이것은 "items"에 저장될 수도 있다. 아니면 Room에 저장될수도있다. TodoScreen은 상태가 관리되는 방식과 완전히 분리된다.
State hoisting is a pattern of moving state up to make a component stateless.
When applied to composables, this often means introducing two parameters to the composable.
- value: T – the current value to display
- onValueChange: (T) -> Unit – an event that requests the value to change, where T is the proposed new value
State 호이스팅은 component stateless를 위해 상태를 이동시키는 디자인 패턴이며, 이에대한 호출은 위와같은 클로져 방식으로 동작한다.
상태에 대한 동작은 viewModel에 delegating할 수 있으며, 이를 구현할 것이다.
Do : 먼저 TodoActivity에 컴포저블을 onCreate 시점에 call해주는 함수를 아래와 같이 만들어준다.
StateCodelabTheme, Suface는 정할수 있는 Theming Form이다.
위와 같이 구성하면 이제, Viewmodel, TodoActivityScreen 그리고 TodoScreen에 대한 dynamic 구성을 하여 unidirectional data flow만 구성한다면 우리가 원하는 모든 것을 만들 준비가 되 ㄴ것이다.
일단 실행하면 아래와 같은 형태로 컴포저블이 표현되며, onAdditem, onRemoveItem을 뷰모델을 통해 구현하면된다.
Kotlin tip
You can also generate a lambda that calls a single method using the method reference syntax. This will create a lambda out of a method call. Using method reference syntax, onAddItem above can also be expressed as
onAddItem = todoViewModel::addItem.
This codelab will use the method reference syntax for future calls.
메소드 콜을 위와 같은 형식 "::" 으로 표현할 수 있습니다.
Do : todoItems LiveData 를 observeAsState를 사용하여 TodoActivityScreen 가 observing하도록 만들어주고 Viewmodel의 메소드를 중계하도록 만들어 줍니다.
This line will observe the LiveData and let us use the current value directly as a List<TodoItem>.
위의 items는 LiveData를 observing할 것이고 List<TodoItem>으로서 현재 값을 직접적으로 사용할 것이다.
There's a lot packed into this one line – so let's take it apart:
- val items: List<TodoItem> declares a variable items of type List<TodoItem>
- todoViewModel.todoItems is a LiveData<List<TodoItem> from the ViewModel
- .observeAsState observes a LiveData<T> and converts it into a State<T> object so Compose can react to value changes : LiveData<T>를 관측하여 State<T> 객체로 Converting하여 Compose가 어떠한 변화에도 react하도록 만드는 메소드.
- listOf() is an initial value to avoid possible null results before the LiveData is initialized, if it wasn't passed items would be List<TodoItem>? which is nullable.
- by is the property delegate syntax in Kotlin, it lets us automatically unwrap the State<List<TodoItem>> from observeAsState into a regular List<TodoItem> : by를 통해 자동으로 State<List<TodoItem>>을 observeAsState에서 List<TodoItem>으로 unwrapping 한다.
실행하면 위와같이 된다.
위의 실행 메커니즘을 잠시 알아보자.
1> TodoScreen에서 "Add random item" 버튼을 클릭했을때 클로저로 아래의 onAddItem 메소드가 실행된다.
이는 generateRandomTodoItem 메소드를 실행하며, Dataclass TodoItem을 반환한다.
그렇게 반환된 TodoItem은 TodoActivityScreen의 TodoScree의 onAddItem의 람다로 들어오는데 이때 todoViewmodel.addItem을 호출하여 이를 등록해준다.
LiveData todoItems는 TodoScreen의 items에 의해 동기화되어있고, 이는 LazyColumn의 items에 의해 추가된다.
인자로 TodoRow에 TodoItem을 전달하고 이를 drawing하는 과정이 TodoRow에 정의되어있으며 이는 stateless하게 정의되어 부수효과없이 반복적으로 수행된다.
Delete도 마찬가지 방식으로 동작함을 확인할 수 있다.
5. Memory in Compose
Random을 composable에 적용한다.
[아래의 구현 iconAlpha 구현은 버그가 있을 것이고, 코드랩이 진행됨에 따라 수정과정이 나올것이다]
iconAlpha 에 randomTint를 아래와 같이 추가한다.
그렇게 적용하면 아래와 같이 Add random Item을 눌렀을때마다 모든 아이콘들이 랜덤색으로 재배치 되는것을 확인할 수 있다.
이는 randomTint가 recomposition 프로세스과정에서 계속 호출 되기때문이다.
참고 :
Recomposition is the process of calling composables again with new inputs to update the compose tree. In this case when TodoScreen is called again with a new list, LazyColumn will recompose all of the children on the screen
Recomposition is the process of running the same composables again to update the tree when their data changes
recompose시 이를 막기위해서는 "remember"를 사용할수 있으며, 마지막으로 composition되었을때의 tint 값을 기억해놓고 있으면된다.
remember gives a composable function memory 이며, 이는 오직 keys가 바뀌었을때에만 recompute할 수 있다.
이는 singleton object에서의 private val 와 비슷하다고 생각할 수 있다.
따라서 위 코드를 아래와 같이 수정한다.
위와 같이 수정하면 tre
는 아래와 같은 형식이 된다.
remember를 자세히보면 key/calcuation 2가지 특징을 갖고 있다.
- key arguments – the "key" that this remember uses, this is the part that is passed in parenthesis. Here we're passing todo.id as the key.
- calculation – a lambda that computes a new value to be remembered, passed in a trailing lambda. Here we're computing a random value with randomTint().
remember 블록에 의해 recomposition시 randomTint()가 다시 호출되지 않는다
Values remembered in composition are forgotten as soon as their calling composable is removed from the tree.
They will also be re-initialized if the calling composable moves in the tree. You can cause this in the LazyColumn by removing items at the top.
Recomposition of a composable must be idempotent. By surrounding the call to randomTint with remember, we skip the call to random on recomposition unless the todo item changes. As a result, TodoRow has no side-effects and always produces the same result every time it recomposes with the same input and is idempotent.
컴포저블의 Recomposition은 반드시 idempotent(멱등원, (예 : F o F = F) ) 상태여야 한다. remember block으로 둘러싸서 composable 함수의 멱등원 법칙을 보존한다. 그결과 TodoRow는 부수효과 없이 동일 input에 대한 동일 output result를 보일수 있다.
iconAlpha의 임의 지정값을 지정해줄수 있게 하기위해, 아래와 같이, TodoRow의 input으로 올려줄 수 있다.
위와 같이 사용해도 버그가 존재한다.
TodoRow에서 remember하고 있는 값은 스크롤링을 해서 composable이 remove 되는 상황등이 되면 remember 값도 사라지기때문에 lazyColumn에 의해 다시 composing하는 상황에서 randomTint()가 재호출 되는것이다.
Remember stores values in the Composition, and will forget them if the composable that called remember is removed.
This means you shouldn't rely upon remember to store important things inside of composables that add and remove children such as LazyColumn.
For example, animation state for a short animation is safe to remember in a child of LazyColumn, but a Todo task's completion would be forgotten on scroll if remembered here.
이를 해결하기 위해서는 state Hoisting을 사용해야한다.
6. State in Compose
위 2가지 모드에 대해 구현한다. 이를 위해서는 state에 대한 이해가 있어야한다.
먼저 UI에 있는 Editing text는 Stateful하다. state는 EditText에 internal하고 onTextChanged listeners를 통해서 노출되게 되는데, 이는 단방향 플로우방식인 compose와는 맞지 않다. 이에 대한 대안으로써 Stateless composable인 TextField를 사용하면된다. (참고 : 이와같이 여러 Built-in composables는 단방향 플로우를 위해 디자인되어있다)
위 함수는 mutableStateOf에다가 값을 저장하는데 이는 Compose에서 built-in 타입으로 정의된 observable state holder이다.
Any changes to value will automatically recompose any composable functions that read this state.
You declare a MutableState object in a composable three ways:
- val state = remember { mutableStateOf(default) }
- var value by remember { mutableStateOf(default) }
- val (value, setValue) = remember { mutableStateOf(default) }
위와 같이 3가지 방식으로 mutableState 객체를 선언할 수 있다.
TodoInputTextField 내부에 internal state를 위와 같은 형식으로 만들고 아래와 같은 composable fun에서 이를 사용할 수 있다.
위 정의된 fun을 TodoScreen에서 호출하기 위해 아래와 같이 구현한다.
이때 TodoItemInputBackground는 미리 구현되어있으며 아래와 같다
참고 : TodoItemInput을 아래와 같이 Preview를 통해 확인할 수 있다.
.
실행시 위와 같이 된다.
Make the button click add an item
Now we want to make the "Add" button actually add a TodoItem. To do that, we'll need access to the text from the TodoInputTextField.
If you look at part of the composition tree of TodoItemInput you can see that we're storing the text state inside of TodoInputTextField.
Add 버튼을 실제로 구현해본다. Text에 적힌 값을 알기 위해서는 TodoInputTextField의 내부 text state에 저장된 값을 보아야한다.
단방향성 플로우를 생각했을때, 위 구조에서 onClick 버튼 클릭시 "text"를 expose하기 어렵다
This structure won't let us wire the onClick up because onClick needs to access the current value of text.
What we want to do is expose the text state to TodoItemInput – and use unidirectional data flow at the same time.
Unidirectional data flow applies both to high level architecture and the design of a single composable when using Jetpack Compose.
Here, we want to make it so that events always flow up and state always flows down.
위 구조를 위해서 아래와 같이 변경한다.
즉 Editbutton에 있던 text를 lifting(hoisting)하여 TodoItemInput의 Remeber로 승급시킨다. 이때 클로져 형식으로 값을 보낼 수 있다. 이렇게 만든다면 TodoInputTextField는 Stateless 상태가 된다.
To start hoisting state, you can refactor any internal state T of a composable to a (value: T, onValueChange: (T) -> Unit) parameter pair.
Hoisting state를 시작하기 위해서, 컴포저블의 내부 state T를 (value: T, onValueChange: (T) -> Unit) parameter 쌍으로 refactoring한다.
즉, 일단 TodoInputTextField를 아래와 같이 수정한다(remember를 제외)
위 코드에서 value/onValueChange를 text/onTextChange로 추가한다.
State는 아래와 같은 중요한 특징을 가지고 있다
State that is hoisted this way has some important properties:
- Single source of truth – by moving state instead of duplicating it, we're ensuring there's only one source of truth for the text. This helps avoid bugs.
- Encapsulated – only TodoItemInput will be able to modify the state, while other components can send events to TodoItemInput. By hoisting this way, only one composable is stateful even though multiple composables use the state.
- Shareable – hoisted state can be shared as an immutable value with multiple composables. Here we're going to use the state in both TodoInputTextField and TodoEditButton.
- Interceptable – TodoItemInput can decide to ignore or modify events before changing its state. For example, TodoItemInput could format :emoji-codes: into emoji as the user types.
- Decoupled – the state for TodoInputTextField may be stored anywhere. For example, we could choose to back this state by a Room database that is updated every time a character is typed without modifying TodoInputTextField.
TodoItemInput를 아래와 같이 수정한다.
즉 TodoInputTextField의 value와 ChangeValue를 정의해둔 꼴로 바꾸고 remeber를 TodoItemInput으로 올린 모습이다.
TodoItemInput의 TodoEditbutton을 아래와 같이 수정한다.
클릭했을때 TodoItem(text)를 onItemComplete 클로져를 통해 최상위로 올리도록 설계가 되어 있음을 확인할 수 있다.
7. Dynamic UI based on state
UI를 다이나믹하게 바꿀 수 있다.
TodoItemInput에서 아래와같이 icon에 관련한 state variable을 만들어준다.
"icon" variable은 현재 선택된 icon에 대한 state를 가지고 있을 것이다.
iconsVisible는 TodoItemInput에 새로운 state를 add하지 않는다. 이를 직접적으로 바꿀 방법을 제공하지 않았기때문이다. 대신, text에 의해 완전히 의존함으로써 recomposition에서 text의 값이 무엇이됐든, iconsVisible은 적절한 UI를 보여주도록하는데 사용될 수 있을 것이다.
TodoItemInput에 다른 state를 icons을 컨트롤하기 위해 사용할수도 있지만, text에 전적으로 based 되기위해 위와같이 구성하였다(2개의 스테이트를 쓰면 더 쉬울수도있음). 대신에, 1개의 truth의 source를 사용할 것이고... 이는 text가 된다.
compose는 동적으로 composition을 변화시키기때문에 visibility 특성이 존재하지않는다. 대신 특정 조건에 대해 composable을 remove함으로써 visibility를 조정한 것과 같은 효과를 낼 수 있다.
onClick 이벤트 리스너를 만들어준다.
위의 코드를 아래와 같이 수정한다.
키보드 이미지를 ime로 컨트롤할 수 있다.
아래 이미지의 파란색 V 부분을 눌렀을때 Done 상태가 되도록하여 키보드를 닫아보자.
TodoInputText를 사용하면 onImeAction 이벤트로 imeAction에 응답할 수 있다.
이벤트를 변수로 추출하여 TodoInputText의 onImeAction과 TodoEditButton의 onClick 모두에 사용하여 구현한다.
TodoItemInput를 아래와 같이 수정한다.
위와 같이 Compose는 UI를 컨트롤하는 부분에 대해서도 추상화를 할 수 있다는 장점이 있다.
키보드를 컨트롤하기 위해서 TextField는 2개의 파라미터를 제공한다.
- keyboardOptions - used to enable showing the Done IME action
- keyboardActions - used to specify the action to be triggered in response to specific IME actions triggered - in our case, once Done is pressed, we want submit to be called and the keyboard to be hidden.
To control the software keyboard, we'll use LocalSoftwareKeyboardController.current.
Because this is an experimental API, we'll have to annotate the function with @OptIn(ExperimentalComposeUiApi::class).
즉, TodoInputText의 onImeAction은 아래 키보드의 V 버튼에 대한 액션에 대한 이벤트를 처리하고
아래의 TodoEditButton은 Add text버튼을 클릭했을때의 이벤트를 처리한다.
위 둘다 onItemComplete의 클로져를 상위로 전달한다.
참고 : Keyboard option의 imeAction icon을 조정하고 그 이벤트를 전달 받을수 있다.
위와 같이 Next를 호출하면 키보드는
위와 같이 표현된다
8. Extracting stateless composables
composable을 stateless하게 만드는 방법에 대한 소개.
위와 같이 에디팅을 하는 기능을 만들어본다.
일단 기존의 기능을 Stateless하게 만들어보자.
UI파트를 셀렉트하고 alt ctrl m 을 누르면 리팩터의 extract function 창이 뜬다.
Ok를 누르면 stateless한 function을 생성할 수 있다.
기존의 TodoItemInput 이름을 TodoItemEntryInput 로 변경하고 stateless function의 이름을 TodoItemInput으로 변경하였다.
이로써 stateful composable을 stateless와 stateful 2개로 나눌수 있게 되었다. 이부분은 compose를 사용하는데 있어 매우 중요한 과정이다. UI-related코드와 그렇지 않은 코드에 대한 분리를 통해 작업 효율을 높일수 있다.
9. Use State in ViewModel
위 editing 모드를 만들어본다.
.State tree는 아래와 같다
TodoItemEntryInput와 TodoInlineEditor
가 editor의 state를 알아야하므로 적어도 TodoScreen까지 state를 hoist해야한다.editor는 list 옆에 있어야한다. 그래야 list에 개입할 수 있기때문이다. TodoViewmodel은 list가 live형태로 있으므로, 바로 이곳이 우리가 edit기능을 추가할 장소가 된다.
When hoisting state, there are three rules to help you figure out where it should go
- State should be hoisted to at least the lowest common parent of all composables that use the state (or read)
- State should be hoisted to at least the highest level it may be changed (or modified)
- If two states change in response to the same events they should be hoisted together
You can hoist state higher than these rules require, but underhoisting state will make it difficult or impossible to follow unidirectional data flow.
state는 가장 낮은 공통 부모로부터 출발하여 가장 높은 단계까지 hoist되어야한다.
Convert TodoViewModel to use mutableStateListOf
TodoViewmodel에 editor를 위한 state를 add한다.
ViewModel에 있는 mutableStateListOf에 대한 사용을 하여 어떻게 compose targeting에 있어 LiveData<List>를 simplifying 시킬 것인지를 확인한다.
mutableStateListOf 는 MutableList를 observable하게 만들어주는데 그 구현은 아래와 같다.
위의 코드를 아래와같이 고쳐준다. MutableListData(listOf(..)) 를 mutableStateListOf 로 고쳐준 꼴이다
By specifying private set, we're restricting writes to this state object to a private setter only visible inside the ViewModel.
The work done with mutableStateListOf and MutableState is intended for Compose.
If this ViewModel was also used by the View system, it would be better to continue using LiveData.
viewsystem을 그대로 쓰고 있다면 Livedata형식을 취하고, compose 형식을 사용하겠다면, mutableStateListOf and MutableState을 쓰면된다.
위와 같이 수정한후 TodoActivityScreen에서 Viewmodel을 사용하도록 아래와 같이 설정한다.
수정후 아래와 같다.
Define editor state
Now it's time to add state for our editor. To avoid duplicating the todo text – we're going to edit the list directly. To do that, instead of keeping the current text that we're editing, we'll keep a list index for the current editor item.
중복 방지를 위해 editing은 list에 직접개입하여 수정하도록 설계한다.
사용자가 click한 list의 position을 알 수 있도록 변수를 하나두고 인덱스를 넣어준다.
// private state
private var currentEditPosition by mutableStateOf(-1)
그리고나서 currentEditItem을 compose에 노출시킨다.
컴포저블이 currentEditItem을 호출할 때마다 todoItems와 currentEditPosition 모두에 대한 변경 사항을 Observing한다. 둘 중 하나가 변경되면 컴포저블은 getter를 다시 호출하여 새 값을 가져온다.
다음으로, 아이템이 선택되었을때, 에디팅이 완료되었을때, 에디팅 중일때. 이렇게 3개의 event를 만들어야한다.
The events onEditItemSelected and onEditDone just change the currentEditPosition. By changing currentEditPosition, compose will recompose any composable that reads currentEditItem.
onEditItemSelected와 onEditDone은 단지 currentEditPosition만을 바꾼다. currentEditPosition을 바꿈으로써 compose는 any composable을 recompose할 것이다.
End editing when removing items
Update the removeItem event to close the current editor when an item is removed.
10. Test State in ViewModel
생략.
11. Reuse stateless composables
Pass the state and events to TodoScreen
9장에서 TodoViewModel에 우리가 필요한 모든 이벤트와 state에 대한 정의를 했다.
이를 TodoScreen으로 가져와서 배치하면된다.
위를 아래와 같이 업데이트해준다.
Preview도 업데이트해준다.
TodoActivityScreen 를 아래와 같이 업데이트해준다.
위를 아래와 같이 업데이트한다.
Define an inline editor composable
Create a new composable in TodoScreen.kt that uses the stateless composable TodoItemInput to define an inline editor.
Use the inline editor in LazyColumn
In the LazyColumn in TodoScreen, display TodoItemInlineEditor if the current item is being edited, otherwise show the TodoRow
fun TodoScreen() 에다 위의 TodoItemInlineEditor를 적용한다.
위를 아래와 같이 변경한다
The LazyColumn composable is the compose equivalent of a RecyclerView. It will only recompose the items on the list needed to display the current screen, and as the user scrolls it will dispose of composables that left the screen and make new ones for the elements scrolling on.
LazyColumn is for displaying large lists of items.
It only composes the items currently on the screen, and disposes of them as soon as they leave. Unlike RecyclerView it doesn't need to do any recycling – compose handles the creation of new composables in a more efficient manner.
LazyColumn 구성 가능은 RecyclerView에 해당하는 구성입니다. 현재 화면을 표시하는 데 필요한 목록의 항목만 재구성하고 사용자가 스크롤할 때 화면을 떠난 구성 요소를 삭제하고 스크롤되는 요소에 대해 새 구성 요소를 만듭니다.
LazyColumn은 큰 항목 목록을 표시하기 위한 것입니다. 현재 화면에 있는 항목만 구성하고 떠나는 즉시 폐기합니다. RecyclerView와 달리 재활용을 수행할 필요가 없습니다. compose는 보다 효율적인 방식으로 새 컴포저블 생성을 처리합니다.
Swap the header when editing
Next, we'll finish the header design and then explore how to swap out the button for emoji buttons that the designer wants for their neo-modern interactive design.
Go back to the TodoScreen composable and make the header respond to changes in editor state. If currentlyEditing is null, then we'll show TodoItemEntryInput and pass elevation = true to TodoItemInputBackground. If currentlyEditing is not null, pass elevation = false to TodoItemInputBackground and display text that says "Editing item" in the same background.
엘레베이션을 현재 에디팅하고 있는지를 체크하여 inputbackground에대한 설정값을 변경할 수 있다.
위를 아래와 같이 수정한다
12. Use slots to pass sections of the screen
마지막으로 editing을할때 오른쪽 과같이 아이콘이 나오게 해보자
composable에서는 pre-configured button을 정의해서 이를 쉽게 처리할 수 있다.
The pattern to pass a pre-configured section is slots. Slots are parameters to a composable that allow the caller to describe a section of the screen. You'll find examples of slots throughout the built-in composable APIs. One of the most commonly used examples is Scaffold.
Scaffold라는 예제를 통해 Slots이라는 파라미터로 쉽게 reusable한 형태로 구성하는것을 알아본다
위와 같이 topBar, bottomBar 포지션에 어떠한 컴포저블 객체를 넣어서 표시하고 싶으면 표시할 수 있는 Slot이란 형태로 Composable을 구성할 수 있다
Define a slot on TodoItemInput
위와 같이 Generic Slot인 buttonSlot을 하나 추가한다.
아래와 같이 Box를 하나 정의하고 Column 포지션에다가 buttonSlot() composable을 하나 넣어주는 형태를 구성할 수 있다
Update stateful TodoItemEntryInput to use the slot
Now we need to update the callers to use the buttonSlot. First let's update TodoItemEntryInput:
마지막으로 TodoScreen.kt에 아래의 button slot을 구현해준다
ps) 포스팅을 하면서.. 중간중간에 바뀐 부분들을 모두 올리진 못했습니다. 중요맥락을 파악해서 공식 가이드의 start/finish 코드를 꼭 리뷰해보세요
'Android Dev > Compose' 카테고리의 다른 글
Compose - LazyColumn + Counting Method + Firebase + Flow 예제 (0) | 2021.09.04 |
---|---|
Compose Layout trial (0) | 2021.08.31 |
Compose - LazyColumn Refresh ( mutableStateOf / PagingSource) / Get Index 구현 (0) | 2021.08.28 |
JetPack Compose(3) - Advanced State and Side Effects (0) | 2021.08.15 |
JetPack Compose(2) - Navigation (0) | 2021.08.11 |