본 포스팅은 DroidKaigi 2017 ~ 💪🏻Activityを改善した話 을 기본으로 번역하여 작성했습니다
제 일본어 실력으로 인하여 오역이나 오타가 발생할 수 있습니다.
실제 슬라이드의 일본어
부분을 번역했다는 점 양해바랍니다.
Activity를 개선한 이야기
@lvla0805
Moyuru Aizawa
Lead Kotlin engineer of Pairs Div. Eureka, Inc.
eureka
Pairs
먼저 이상적인 상대를 찾습니다
관심이 가는 상대에게 “좋아요!”를 보낸다
매칭 후, 메시지 교환
샘플 코드에 대해
Pairs는 Java/Kotlin이 섞여 있습니다. 그래서, 샘플 코드에도 두 언어가 이용되고 있습니다.
Kotlin이 어려운 친구 여러분
도서관에서 Kotlin에 대해 찾아봐요!
Instance 생성
// Java
final Serval serval = new Serval();
Kaban kaban = new Kaban();
// Kotlin
val serval = Serval()
var kaban = Kaban()
// Java
public class JapariBus {
private final Trailer trailer;
private final Container container;
public JapariBus(Trailer trailer, Container container) {
this.trailer = trailer;
this.container = container;
}
}
// Kotlin
class JapariBus(private val trailer: Trailer,
private val container: Container)
Lambda
// Java
observable.map(new Fun1<Foo, Bar>() {
@Override
public Bar call(Foo foo) {
return foo.toBar();
}
});
// Kotlin
observable.map { foo -> foo.toBar() }
본론
Pairs에는 Activity가 가득(했다)!
사용 기술에 대해
기술개요
Languages | Java/Kotlin |
Network | Retrofit2, OkHttp3 |
O/R Mapper | Orma |
Reactive Programming | RxJava1 |
Dependency Injection | Dagger2 |
Activity?
비즈니스 로직
비즈니스 로직
if (isPaidUser) {
// 유료 회원만 사용할 수 있는 기능을 유효화한다
} else {
// 유료 회원만 사용할 수 있는 기능을 무효화한다
}
비즈니스 로직
if (isLikedByPartner) {
// 상대방으로부터 좋아요! 를 받았을 때 상태
} else if (iisLikedByMe) {
// 좋아요! 를 했을 상태
} else if (isLikedWithMessage) {
// 메시지 포함한 좋아요! 를 했을 상태
} else if (isLooked) {
// 봐줘! 를 했을 상태
} else if (isLookedWithMessage) {
// 메시지 포함 봐줘! 를 했을 상태
} else if (isHidden) {
// 비표시 설정을 한 직후
} else if (isBlocked) {
// 차단 설정을 한 직후
} else if (...) {
...
}
비즈니스 로직
IO
IO
userClient.fetchMathingUsers()
.subscribeOnIO()
.doOnNext { users -> userDao.insertAll(users) }
.onError { userDao.findAll() }
.observeOnMain()
.subscribe(
{ users -> showUsers(users) },
{ ... })
어쨌든 라인이 많아 괴롭다
적은 Activity 만이 아니다!
힘든 점
엄청난 책임
엄청난 책임
class UserConverter {
fun convert(res: MatchingUsersResponse): List<User> {
return res.users.map { resUser -> User(resUser.name, ...) }
}
}
그런데
엄청난 책임
class UserConverter {
fun convert(res: MatchingUsersResponse): List<User> {
return res.users.map { resUser -> User(resUser.name, ...) }
.filter { ... }
}
}
神 클래스
神 클래스
XxxManager
神 클래스
神 클래스
Static 아저씨
static 아저씨
static 아저씨
public static Observable<List<User>> fetchMathingUsers() {
return PairsClient.getRetrofit()
.create(MatchingUsersService.class)
.fetch()
...
}
static 아저씨
static 아저씨
class MatchingUsersClient(
private val service: MatchingUsersService) {
fun fetch(): Observable<List<User>> {
return service.fetch().....
}
}
위험해! 어떻게 해야 한다!
EMERGENCY!!
Emergency
테스트 코드를 쓰자!
… 흠
못 적지않나?
테스트 못하지요
과제
대대적인 리팩터링
Before
책임 분할
After
Retrofit이 구현하는 Interface
Server와 데이터 송수신
DB와 데이터 송수신
데이터 수신/보존
비즈니스 로직
비즈니스 로직을 사용, Activity/Fragment 조작
View의 조작, Activity 의존하는 기능 조작
Matching
Cient
Client
Client
class UserClient(private val service: UserService) {
fun fetchMatchingUsers(offset: Int): Observable<List<User<< {
return service.fetchMatchingUsers(offset)
.map { response -> UserConverter.convert(response) }
}
}
Client Test
val userService = mock<UserService>()
val client = Userclient(userService)
userService.invoked.theReturn(...)
client.fetchMathingUsers().test().run {
...
}
DAO
DAO
DAO
class UserDao(private val orma: OrmaDatabase) {
fun insert(users: List<User>) {
orma.transactionSync {
orma.prepareInsertIntoUser(OnConflict.REPLACE)
.executeAll(contributors)
}
}
fun findAll(): Observable<List<User>> {
return orma.selectFromUser()
.executeAsObservable()
.toList()
}
}
Dao Test
val dao = UserDoa(orma)
dao.insert(...)
orma.selectFromUser()
.executeAsObservable()
.toList()
.test()
.run {
...
}
orma.insertIntoUser()...
dao.findAll().test().run {
...
}
Repository
Repository
UserRepository
class UserRepository (
private val dao: UserDao,
private val userClient: UserClient) {
fun getMatchingUsers(offset: Int): Observable<List<User>> {
return userClient.fetchMatchingUsers(offset)
.doOnNext { users -> dao.insert(users) }
.onErrorResumeNext { dao.findAll() }
}
}
UseCase
UseCase
MatchingUsersUseCase
class MatchingUsersUseCase(private val repo: UserRepository) {
fun getUsers(offset: Int) = repo.getMatchingUsers(offset)
fun getFacebookPublicSetting(me: Me, user: User): FacebookPublicSetting {
return if (shouldShowMeFacebook(me, user)) {
if (shouldShowPartnerFacebook(user)) {
FacebookPublicSetting.BOTH
} else {
FacebookPublicSetting.ONLY_ME
}
} else {
if (shouldShowPartnerFacebook(user)) {
FacebookPublicSetting.ONLY_USER
} else {
FacebookPublicSetting.NEITHER
}
}
}
}
Presenter
Presenter
Presenter
class MatchingPresenter(
private val contract: Contract,
private val useCase: MatchingUserUseCase) {
fun init() {
useCase.getUsers().applySchedulers()
.doOnSubscribe { contract.showProgress() }
.doOnUnsubscribe { contract.hideProgress() }
.subscribe(
{ users -> contract.showMatchingUsers(users) },
{ ... }
)
}
interface Contract {
fun showMatchingUsers(users List<User>)
fun showProgress()
fun hideProgress()
}
}
)
Activity, Fragment
Activity/Fragment
Activity/Fragment
class MatchingFragment: Fragment(), MatchingPresenter.Contract {
...
override fun onViewCreated(...) {
...
presenter.init()
}
override fun showMatchingUsers(users List<User>) {
listView.adapter = MatchingUsersAdapter(activity, users)
}
override fun showProgress() {
progress.toVisible()
}
override fun hideProgress() {
progress.toGone()
}
}
Dependency Injection
의존관계 Graph
DI
그래!
이런 느낌으로 하자!
어떻게 진행했는가
어떻게 진행했는가
어떻게 진행했는가
어떻게 진행했는가
## 132p
힘든 점
## 133p
힘든 점
1.5개월 후…
Release
개선 후
이 구현의 장점
이 구현의 단점
Conclusion
정리
comments powered by Disqus
Subscribe to this blog via RSS.