Android2022. 1. 26. 11:34

DataStore는 SharedPreferences를 대체하기 위해서 고안되었다.
SharedPreferences를 대체해야 겠다는 생각을 구글개발자들은 왜 했을까?


1. SharedPreferences는 UIThread에서 호출이 가능한데 기본적으로 disk I/O operation이기 때문에 UITHread에 대해 block을 유발하게 된다. 때에 따라서는 이점이 anr을 유발할수도 있다.
2. 런타임 파싱에러를 발생시킬 수 있다.

 

이런 문제점이 마음에 들지 않았던 어느 개발자가 DataStore를 만들었다.


DataStore는 두가지 구현을 제공하는데 모두 파일로 저장하고 Dispatchers.IO에서 동작한다.

 

1. Preference DataStore는 SharedPreference와 유사하고 key/value쌍으로 저장한다.
2. Proto DataStore는 Protocol buffer로 스키마를 정의하고 강력한 데이타타입체크를 지원한다. protocol buffer는 더 빠르고 작고 단

순하며 다른 XML같은 포맷보다 덜 모호하다. protocol buffer 정의하고 시리얼화 매커니즘을 추가로 배워야 하지만 강력한 타입스키마체크라는 장점때문에 충분히 배울 가치가 있다고 본다.

Room이라는 또 다른 Store를 제공하는 라이브러리가 있으며 DataStore가 작고 단순한 데이터셋을 지원하는 반면 Room은 부분업데이트나 참조무결성을 지원한다.  정확하게 어떤 다른 특성이 있는지는 더 조사해 보아야 겠다.

// Preferences DataStore 사용시
implementation "androidx.datastore:datastore-preferences:1.0.0-alpha01"

// Proto DataStore 사용시
implementation  "androidx.datastore:datastore-core:1.0.0-alpha01"

Proto DataStore를 사용하고자 한다면 스키마를 proto file로 /app/src/main/proto 폴더에 저장한다. 스키마정의 링크에서 자세한 정의 방법을 더 숙지해야 한다.

syntax = "proto3";

option java_package = "<your package name here>";
option java_multiple_files = true;

message Settings {
  int my_counter = 1;
}

 

DataStore 생성

Preference DataStore 생성

// with Preferences DataStore
val dataStore: DataStore<Preferences> = context.createDataStore(
    name = "settings"
)

Proto DataStore 생성

protobuff file명과 Serializer의 구현체를 제공해야 한다. 

object SettingsSerializer : Serializer<Settings> {
    override fun readFrom(input: InputStream): Settings {
        try {
            return Settings.parseFrom(input)
        } catch (exception: InvalidProtocolBufferException) {
            throw CorruptionException("Cannot read proto.", exception)
        }
    }

    override fun writeTo(t: Settings, output: OutputStream) = t.writeTo(output)
}


// with Proto DataStore
val settingsDataStore: DataStore<Settings> = context.createDataStore(
    fileName = "settings.pb",
    serializer = SettingsSerializer
)

DataStore 읽기

Preference DataStore 읽기

val MY_COUNTER = preferencesKey<Int>("my_counter")
val myCounterFlow: Flow<Int> = dataStore.data
     .map { currentPreferences ->
        // Unlike Proto DataStore, there's no type safety here.
        currentPreferences[MY_COUNTER] ?: 0   
   }

Proto DataStore 읽기

val myCounterFlow: Flow<Int> = settingsDataStore.data
    .map { settings ->
        // The myCounter property is generated for you from your proto schema!
        settings.myCounter 
    }

 

DataStore 쓰기

Preference DataStore 쓰기

suspend fun incrementCounter() {
    dataStore.edit { settings ->
        // We can safely increment our counter without losing data due to races!
        val currentCounterValue = settings[MY_COUNTER] ?: 0
        settings[MY_COUNTER] = currentCounterValue + 1
    }
}

 

Proto DataStore 쓰기

suspend fun incrementCounter() {
    settingsDataStore.updateData { currentSettings ->
        // We can safely increment our counter without losing data due to races!
        currentSettings.toBuilder()
            .setMyCounter(currentSettings.myCounter + 1)
            .build()
    }
}

 

Data를 Modeling할때 DataStore를 사용해서 해보는것도 좋은 경험이 될것 같다.

언제 해볼 수 있을지 모르겠지만 .. ㅠㅜ 

Posted by 삼스
Android2021. 12. 23. 11:22

ViewModel
- 액티비티, 프래그먼트의 복잡한 라이프사이클과 무관하게 데이터를 보관해줌
- 네트웍이나 저장소등의 레포지트리를 통해서 데이터를 수집
- ViewModel을 사용하는 액티비티나 프래그먼트에서는 ovserve를 사용하여 데이터가 변경될때 이벤트를 받을 수 있으며 이 때 화면에 데이터를 바인딩하면 된다.

 

Java

public class MyViewModel extends ViewModel {  
    private MutableLiveData<List<User>> users;  
    public LiveData<List<User>> getUsers {  
        if (users == null) {  
            users = new MutableLiveData<List<User>>();  
            loadUsers();  
        }  
        return users;  
    }  

    private void loadUsers() {  
        // load from local storage or network  
    }  
}  

Kotlin

class MyViewModel : ViewModel() {  
    private val users: MutableLiveData<List<User>> by lazy {  
        MutableLiveData<List<User>>().also {  
            loadUsers()  
        }  
    }  
    fun getUsers() : LiveData<List<User>> {  
        return users  
    }  

    private fun loadUsers {  
        // load from local storage or network  
    }  
}

ViewModel 사용

Java  
public class MyActivity extends AppCompatActivity {  
    public void onCreate(Bundle savedInstanceState) {  
        MyViewModel model = new ViewModelProvider(this).get(MyViewModel.class);  
        model.getUsers().observe(this, users -> {  
            // update UI  
        });  
    }  
}  

Kotlin  
class MyActivity : AppCompatActivity() {  
    override fun onCreate(savedInstanceState: Bundle?) {  
        val model = MyViewModel by viewModels()  
        model.getUsers().observe(this, Observer<List<User>> { users ->   
            // update UI  
        })  
    }  
}

 

 

MyActivity가 단말을 가로로 돌려서 종료되거나 시스템이 강제로 종료시키는 등의 상황이 발생하더라고 MyActivity가 다시 생성될 때 이전의 MyViewModel의 인스턴스를 유지한다.


ViewModel은 View나 Activity, Fragment같은 Lifecycle객체 등을 포함하는 클래스를 참조하면 메모리릭이 발생할 수 있다.
만일 ViewModel이 Context가 필요한 경우가 있다면 AndroidViewModel을 상속한 후 Application객체를 생성자에 포함시키는 방법을 추천한다.

Posted by 삼스
iOS2021. 6. 11. 14:33

앞서 안드로이드에서 Coroutine과 CompletableFuture를 알아 보았다.

제목에서 보면 알수 있듯이 개발자들은 지속적으로 게을러지기 위해 노력하고 있다는것을 알 수 있다.

비동기 호출도 가장 적은 타이핑을 목표로 하고 있다.

 

비동기 처리를 하는 가장 고전적인 방법은 콜백함수를 인자로 하여 처리과 완료되면 콜백으로 처리결과를 전달하여 다음 단계로 넘어가도록 하는 방법이 되겠다.

알다시피 이는 콜백지옥을 수반하게 되며 이게 싫었던 개발자는 Rx나 CompletableFuture같은것들을 고안하고 사용하기 시작한다.

 

그것도 싫었던 그들은 Coroutine같은것을 다시한번 고안해 냈으며 마치 동기호출을 하는 코드처럼 비동기코드를 짜기 시작했다.

 

안드로이드에서는 Coutine으로 아래와 같이 마치 동기호출처럼 비동기 함수들을 호출할 수 있다.

suspend iWantEatRamyeon(money) {
  try {
    val ramyeon = buyRamyeon(money)
    val pot = getPot()
    val cookedRamyeon = doBoilPot(ramyeon, pot)
    val ramyeonInDish = moveToDish(cookedRamyeon)
    eatIt(ramyeonInDish)
  } catch (e: Exception) {
    // 실패케이스 처리
  }
}

 

 

iOS에서는 swift 5에서 아래와 같이 사용 가능하다.

func iWantEatRamyoen(money) async {
    val ramyeon = await buyRamyeon(money: money)
    val pot = await getPot()
    val cookedRamyeon = await doBoilPot(ramyeon:ramyeon, pot: pot)
    val ramyeonInDish = await moveToDish(ramyeon: cookieRamyeon)
    eat(ramyeon: remyeonInDish)
    print("Server response: \(response)")
}

 

async 메서드에서 비동기메서드로 호출하려면 메서드 정의시 아래와 같이 정의해야 한다.

 

func buyRamyeon(money: money) async -> Ramyeon {
	// get a ramyeon 
	var ramyeon = getRamyeon()
	return ramyeon
}

 

DispatchQueue와 블럭코딩을 이용하여 위와 동일한 로직을 작성해보자.

그러면 위 코드가 얼마나 코딩량을 줄여주는지 알게 될것이다.

 

개발자는 게을러져야 하나보다... 머리는 빼고...

Posted by 삼스