본문으로 바로가기
반응형

Android Libraries - Architecture Components

안드로이드 O OS와 함께 새로운 라이브러리가 추가되었습니다.

  • Lifecycle을 handling 할수있는 방법
  • LiveData
  • ViewModel
  • Room Persistence Libraray
위에 언급된것들을 묶어서 Architecture Components라고 합니다.

여기서는 위 네가지에 대한 대략적인 concept만 얘기합니다. (overview 정도라고 보시면 됩니다.)

원문은 아래 링크에 있으니 참고 바랍니다.


ViewModel

viewModel은 특정 activity나 fragment에 데이터를 제공합니다.
또한 데이터를 로딩하거나, 변경을 알려주는 데이터 로직을 다루는 비지니스 파트와 연동됩니다.
ViewModel은 view에 대해서 알지 못하며, activity 재생성이나 rotation 같은 configuration 변경에 영향을 받지 않습니다.

즉!! UI에 독립적이라고 볼 수 있습니다.

예제)
  • user_proflie.xml: UI를 위한 xml 파일
  • UserProfileViewModel.java: UI에서 사용하는 data를 준비하는 class
  • UserProfileFragment.java: data를 보여주고 사용자와 interaction하는 부분
public class UserProfileViewModel extends ViewModel {
    private String userId;
    private User user;

    public void init(String userId) {
        this.userId = userId;
    }
    public User getUser() {
        return user;
    }
}
public class UserProfileFragment extends LifecycleFragment {
    private static final String UID_KEY = "uid";
    private UserProfileViewModel viewModel;

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        String userId = getArguments().getString(UID_KEY);
        viewModel = ViewModelProviders.of(this).get(UserProfileViewModel.class);
        viewModel.init(userId);
    }

    @Override
    public View onCreateView(LayoutInflater inflater,
                @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.user_profile, container, false);
    }
}


위 코드에서 fragment대신에 LifecycleFragment를 상속받습니다.

추후 android architecture가 안정화 되면 Android Support Library 내부에 있는 Fragment가 LifecycleOwner를 구현할 예정이라고 하네요.

이 세개의 component는 LiveData를 통해서 연결되어 데이터가 변경되면 LiveData가 UI에게 알려줍니다.


LiveData

LivaData는 observable data holder이며, app안의 component들이 명시적으로 dependency를 갖지 않도록 하면서, 변경 사항을 LiveData객체에서 관찰 할 수 있도록 합니다.

또한 app component의 lifecycle state에 따라서 동작하므로 memory leak이 나는것도 방지할 수 있습니다.


위에서 사용한 예제의 ViewModel에서 User객체를 반환하는 부분을 LiveData로 wrapping하여 전달하도록 구성해 보겠습니다.

public class UserProfileViewModel extends ViewModel {
    ...
    // private User user;
    private LiveData<User> user;

    // public User getUser() {
    //     return user;
    // }

    public LiveData<User> getUser() {
        return user;
    }
}


또한 이를 불러오는 부분도 아래와 같이 수정합니다.

@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    String userId = getArguments().getString(UID_KEY);
    viewModel = ViewModelProviderts.of(this).get(UserProfileViewModel.class);
    viewModel.init(userId);
    viewModel.getUser().observe(this, user -> {
      // update UI
    });
}


data가 update되면 fragment의 "//update UI" 부분이 호출됩니다.

또한 람다식의 내부는 (T) -> void signature를 갖는 형태로 구성됩니다.

abstract void onChanged(T t)


중요한 점은 LiveData가 lifecycle을 인지하고 있기 때문에 더이상 data가 필요하지 않게되면 자동으로 reference를 clean up 합니다.

(좀 좋지요??)


LiveData에 정의된 callback은 Fragment가 Active 되었을 때만 callback을 수행합니다.

  • Active 상태: onStart()를 수신하고 onStop()을 수신받지 않은 상태. 따라서 onStart() -> onStop()까지의 상태를 active로 봅니다.
  • onDestory()가 불리면 자동으로 observer를 제거합니다.


Fectching data

만약 데이터를 Web에서 REST API를 통해서 가져온다고 할때  API를 호출하는 부분이 적다면 ViewModel에서 직접 구현 할 수도 있습니다.

다만, 해당 부분이 커질수록 유지관리가 힘들어지고 ViewModel이 너무 큰 책임(responsiblility)를 갖기 때문에 이는 피하는것이 좋지요.

ViewModel의 scope은 Activity나 Fragment의 lifecycle에 묶이므로 lifecycle이 종료되면서 데이터를 모두 날릴수도 있다는 점에서 좋지않습니다.!!

따라서 ViewModel은 data를 fetcing하는 부분을 Repository module에 위임(delegate)합니다.

 

Repository는 data operation을 handling하는 책임(responsibility)를 갖습니다. 따라서 앱에 명확한 API를 제공해야 합니다.

Repository는 data를 어디서 조회하는지와 update 되었을때 호출해야 하는API가 무엇인지를 알고 있으며, 여러 data source를 중재하는 역할을 해야합니다.

여기서 data source란 (persistent model(DB), web service, cache, etc.)을 말합니다.

 

만약 web service에서 데이터를 받는 형태이면 아래와 같이 repository class를 생성합니다.

public class UserRepository {
    private Webservice webservice;
    // ...
    public LiveData<User> getUser(int userId) {
        // This is not an optimal implementation, we'll fix it below
        final MutableLiveData<User> data = new MutableLiveData<>();
        webservice.getUser(userId).enqueue(new Callback<User>() {
            @Override
            public void onResponse(Call<User> call, Response<User> response) {
                // error case is left out for brevity
                data.setValue(response.body());
            }
        });
        return data;
    }
}

Repository 모듈이 필요없어보지만 repository를 사용함으로서 data를 추상화 하여, ViewModel에서는 data가 어떻게 fetch되었는지 알 필요가 없도록 합니다.

따라서 Repository에서는 필요에 따라서 data fetch부분을 다른 구현으로 swap해도 ViewModel에는 영향을 미치지 않습니다.( dependency 측면에서 좋지요~)


Connecting ViewModel and repository

ViewModel과 repository의 연결은 아래와 같이 진행합니다.
public class UserProfileViewModel extends ViewModel {
    private LiveData<User> user;
    private UserRepository userRepo;

    @Inject // UserRepository parameter is provided by Dagger 2
    public UserProfileViewModel(UserRepository userRepo) {
        this.userRepo = userRepo;
    }

    public void init(String userId) {
        if (this.user != null) {
            // ViewModel is created per Fragment so
            // we know the userId won't change
            return;
        }
        user = userRepo.getUser(userId);
    }

    public LiveData<User> getUser() {
        return this.user;
    }
}


위와 같이 구성시 UserProfileFragment를 나갔다가 다시 들어오면 data를 re-fetch해야 합니다. 이는 network 낭비이며 사용자는 해당 query가 끝날때 까지 기다려야 하는 단정이 있습니다. 이를 해결하기 위해 우리는 보통 User object를 caching 합니다.

@Singleton  // informs Dagger that this class should be constructed once
public class UserRepository {
    private Webservice webservice;
    // simple in memory cache, details omitted for brevity
    private UserCache userCache;
    public LiveData<User> getUser(String userId) {
        LiveData<User> cached = userCache.get(userId);
        if (cached != null) {
            return cached;
        }

        final MutableLiveData<User> data = new MutableLiveData<>();
        userCache.put(userId, data);
        // this is still suboptimal but better than before.
        // a complete implementation must also handle the error cases.
        webservice.getUser(userId).enqueue(new Callback<User>() {
            @Override
            public void onResponse(Call<User> call, Response<User> response) {
                data.setValue(response.body());
            }
        });
        return data;
    }
}


Persisting data

데이터를 캐쉬하는 방법은 app이 kill당한 후에는 사용하기 어렵습니다. 이를 막기위해 web service request를 caching을 할 수 도 있으나, 다른 형태의 request를 보내는 경우 데이터가 틀어질 수 있습니다.

ex) user 정보가 아닌 친구 정보를 요청하는 경우
Room 은 최소한의 상용구 코드로 로컬 데이터의 지속성을 제공하는 object mapping library 입니다.. room에서는 schema를 확인하여 query의 유효성 검증을 하기 때문에 잘못된 sql은 compile time에 error가 납니다~
room은 raw query 나 table을 작업하는 기본 구현사항들을 추상화 합니다.
컬렉션이나 조인을 포함하여 DB의 data변경되는것을 observing 하여 LiveData를 이용하여 알려줄 수 있습니다. 또한 명시적으로 thread 제약을 명시할 수 있습니다.
(main thread에서 access하는 하는것과 같은 잦은 실수를 방지)
 
Room을 사용하기 위해서는 먼저 local schema를 갖는 entity class를 작성합니다. class 상단에 @Entity 를 마킹해야 합니다.


@Entity
class User {
  @PrimaryKey
  private int id;
  private String name;
  private String lastName;
  // getters and setters for fields
}


RoomDatabase 를 상속받아 내 database class를 생성합니다.

내가 생성한 database class 역시 abstract이며, Room이 자동으로 내부사항을 구현합니다.

상단에 @Database 와 Entity class를 마킹해 줍니다.

@Database(entities = {User.class}, version = 1)
  public abstract class MyDatabase extends RoomDatabase {
}


DAO를 interface로 만들어서 데이터를 insert하거나 query 합니다.

상단에 @Dao를 마킹해 줍니다.

@Dao
public interface UserDao {
    @Insert(onConflict = REPLACE)
    void save(User user);
    @Query("SELECT * FROM user WHERE id = :userId")
    LiveData<User> load(String userId);
}


Dao interface가 만들어지면 해당 dao를 Database class에 등록합니다.

@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
    public abstract UserDao userDao();
}


query를 하는 load는 LiveData<User>를 반환합니다.

Room은 데이터 변경시점을 알고있기 때문에 변경이 일어나면 LiveData를 이용하여 active한 observer에게 notify 합니다.

 

Repository와 room을 연결합니다.

@Singleton
public class UserRepository {
    private final Webservice webservice;
    private final UserDao userDao;
    private final Executor executor;

    @Inject
    public UserRepository(Webservice webservice, UserDao userDao, Executor executor) {
        this.webservice = webservice;
        this.userDao = userDao;
        this.executor = executor;
    }

    public LiveData<User> getUser(String userId) {
        refreshUser(userId);
        // return a LiveData directly from the database.
        return userDao.load(userId);
    }

    private void refreshUser(final String userId) {
        executor.execute(() -> {
            // running in a background thread
            // check if user was fetched recently
            boolean userExists = userDao.hasUser(FRESH_TIMEOUT);
            if (!userExists) {
                // refresh the data
                Response response = webservice.getUser(userId).execute();
                // TODO check for error etc.
                // Update the database.The LiveData will automatically refresh so
                // we don't need to do anything else here besides updating the database
                userDao.save(response.body());
            }
        });
    }
}


이런 구조의 장점은 UserRepository에서 data를 fetching하는 부분이 변경되더라도 추상화로 인하여 UserProfileViewModel이나 UserProfileFragment에는 영향이 없다는점입니다.
또한, UserRepository를 fake로 바꿔서 UserProfileViewModel을 테스트할 수 있다는 Testing 장점도 갖습니다.
 
정리하자면,
user 정보를 persisted 하게 만들어 놨기 때문에, 사용자가 몇일 있다가 같은 UI에 접근하는경우, 즉각적으로 보여줄 수 있습니다.
반면, repository는 data가 변경될때 마다 background에서 업데이트 됩니다.

The final architecture

결국 위 모든 component들은 아래와 같은 관계를 갖습니다.


반응형