Android Libraries - Architecture Components
- Lifecycle을 handling 할수있는 방법
- LiveData
- ViewModel
- Room Persistence Libraray
ViewModel
- 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
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를 보내는 경우 데이터가 틀어질 수 있습니다.
@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());
}
});
}
}
The final architecture
'개발이야기 > Android' 카테고리의 다른 글
Android Architecture Components #3 - LiveData (0) | 2017.10.15 |
---|---|
Android Architecture Components #2 - Handling Lifecycles (0) | 2017.10.14 |
Android Service간 통신 #3 (1) | 2017.10.12 |
Android Service간 통신 #2 (0) | 2017.10.11 |
Android Service간 통신 #1 (6) | 2017.10.10 |