Architecture Components
Past, Present, Future
GDG Makassar
Muh Isfhani Ghiath (ippang)
Software Engineer, Ecommerce
Tech Lead, Kepul.id
GDE for Android
@isfaaghyth with.isfa.dev
Overview
Outline
What
Why
How
Then?
Architecture?
Overview
What is Architecture
Pattern?
An architectural pattern is a general, reusable
solution to a commonly occurring problem in
software architecture within a given context.
Architectural patterns are similar to software design
pattern but have a broader scope.
Wikipedia
Clean
Architecture
Uncle Bob
Overview
Well architecture app, easier
to:
Test
Make Robust
Expand
Team Develop
Data
Logic
Repository Use Case
Data Separation of concerns
UI Data Services User Interface
Networking
Layered
Architecture
UI Layer
Data Layer
Layered
Architecture
UI Layer
User interactions Text, Images
OS interactions Button/onClick behavior
App layout and screens Text edit fields/user input
Layered
Architecture
Data
UI Layer
Layer
Information in the app Email, subject, body (email)
Databases/network Article title, contents (news)
Model
Brief 🧐
Overview
MVP MVI MVVM MVC
Before
Architecture
Before Architecturing
MainActivit
y
Before
Architecture
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle
savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_kategori_tab, container, false);
ButterKnife.bind(this,v);
setLoading(true);
SharedPreferences sharedPreferences =
PreferenceManager.getDefaultSharedPreferences(getActivity());
String token = sharedPreferences.getString(getString(R.string.key_token), null);
if (token != null){
Retrofit retrofit = APIClient.getClient();
APIService apiService = retrofit.create(APIService.class);
Call<SearchResponse> result = apiService.getProductByKategori("api/v1/product/category/"
+ String.valueOf(getArguments().getInt("data")));
result.enqueue(new Callback<SearchResponse>() {
@Override
public void onResponse(Call<SearchResponse> call, Response<SearchResponse> response) {
...
Before
Architecture
Pros Cons
● Easy to Learn ● Hard to Maintain
● Fast Development ● No Separation of Concern
● Hard to Test
● Not readable
MV
P
Model Changed User Interaction
View (Activity,
Model Presenter
fragment...)
Update UI
Update Model
MV
P
interface MyView {
fun displayMyData(data: Data)
fun hideMyData()
}
class Presenter {
lateinit var view: MyView? View Reference
fun setMyView(myView: MyView) {
this.view = myView
}
fun getData() {
val data = dataRepository.getDataFromNetwork()
view?.displayMyData(data)
}
}
MV
P
Issues
● NullPointerException
● Memory Leaks
MV
P
Pros Cons
● Separation of Concern ● Not Sustainable
● Maintainable ● Slower Development
● Testable ● A lot of Interface as
interaction
● UI references on Presenter
MVVM
Model Changed User Interaction
View (Activity,
Model ViewModel
fragment,...)
Observe data
Update Model
MVVM
ViewModel?
Separated from Android framework component Fragment
Context
Viewmodel
Activity
Resources
MVVM
Has observable data to delegate with View
Observe
Viewmodel Activity
No UI Reference
MVVM
View is Dumb! All logic is handled by ViewModel
override fun onCreate(savedInstanceState: Bundle?) {
...
mathViewModel.onActivityCreated()
btn_answer.setOnClickListener {
mathViewModel.onButtonAnswerClicked(
Integer.valueOf(edt_equals.text.toString())
)
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
mathViewModel.saveViewInstance(uiConfigurationObject)
}
MVVM
Observing its ViewModel data
override fun onCreate(savedInstanceState: Bundle?) {
...
yourViewModel.observableData.observe(
this, Observer { data ->
updateView(data)
})
}
MVVM
Android Architecture
Component
MVVM
Scan here for the
link
Android Architecture
Component
MVVM
Pros Cons
● Separation of Concern ● ViewModel too bloated
● Maintainable ● Hard to maintain in large
● Testable project
● No UI reference in ViewModel ● Not scale as much
MVI? 🤔
Bidirection
MVVM
al
UI Layer
Events Events Data
Data
Data Layer
Bidirection
MVVM
al
Why Bidirectional not suit us anymore?
App driven by the data
View changes has explicitly changes whenever data is
transformed
Each component holds part of the domain state
class ViewModel {
private var _userInfo = mutableStateFlow<User>()
class SampleActivity {
val userInfo get() = _userInfo
fun getUserInfo() {
fun fetchUserInfo() {
viewModel.fetchUserInfo()
viewModelScope.launch {
}
val result = userInfoRepository.userInfo()
_userInfo.value = result
} }
}
}
Unidirection
MVI
al
UI Layer
Events Data
Data Layer
Unidirection
MVI
al
Why you have to choose an Unidirectional?
Humans express themselves as per the senses they perceive.
The app is treated as another being that senses the inputs
and expresses its output.
The app listens to the user actions and modifies the data it has
The changes in the data are reflected through UI as an
expression of the app
class ViewModel {
private var _state = mutableStateFlow<UiState>()
val state get() = _state
private var event = mutableSharedFlow<Event>(Event.Unknown)
fun setEvent(event: Event) {
viewModelScope.launch {
event.collect {
class SampleActivity { when (it) {
is UserInfo -> fetchUserInfo()
fun getUserInfo() { }
viewModel.setEvent( }
Event.UserInfo }
) }
}
private fun fetchUserInfo() {
} _state.update {
it.copy(
userInfo = userInfoRepository.userInfo()
)
}
}
}
MVVM
Pros Cons
● Driven by Event ● Still, too bloated
● Event treats as User Actions ● Hard to manage multiple actions
● Reactive in nature ● Each different features in a single
● Explicit about side-effects presentation
MVVM+MVI? Why not
both 🤩
Introduce, Update Framework! 🎉
Clean
Architecture
Common architectural principles
(1/2)
As Android apps grow in size, it's important to define an architecture
that allows the app to scale, increases the app's robustness, and
makes the app easier to test.
Android Architecture
Component
Clean
Architecture
Common architectural principles
(2/2)
An app architecture defines the boundaries between parts of the app
and the responsibilities each part should have. In order to meet the
needs mentioned above, we should design the app architecture to follow
a few specific principles.
Android Architecture
Component
Objective
s
Why Update Framework?
Strong separate of concern
Extensible; component represents as Update
Lifecycle-aware
Test encapsulated
Agnostic!
Update Befor
Framework e
UI ViewModel
fun doLogin()
fun fetchTodoList()
fun showTicker()
fun fetchOnboarding()
fun insertTodo(todo: Todo)
fun updateUserProfile()
class FooViewModel(
private val loginUseCase: LoginUseCase,
private val userSessionManager: UserSessionManager,
) : ViewModel()
class FooViewModel(
private val loginUseCase: LoginUseCase,
private val userSessionManager: UserSessionManager,
) : ViewModel() {
private val _login = MutableStateFlow<UserLogin>()
val login get() = _login.asStateFlow()
fun doLogin(email: String, password: String) {
runCatching {
loginUseCase
.request(email, password)
.collect { result ->
userSessionManager.set(result)
_login.update { result }
}
}
}
}
class FooViewModel(
private val loginUseCase: LoginUseCase,
private val userSessionManager: UserSessionManager,
private val fetchTodoUseCase: GetTodoUseCase,
private val insertTodoUseCase: SetTodoUseCase
) : ViewModel() {
private val _login = MutableStateFlow<UserLogin>()
val login get() = _login.asStateFlow()
private val _todoList = MutableStateFlow<TodoList>()
val todoList get() = _todoList.asStateFlow()
fun doLogin(email: String, password: String) { ... }
fun fetchTodoList() {
runCatching {
fetchTodoUseCase
.fetch()
.collect { result ->
_todoList.update { result }
}
}
}
}
class FooViewModel(
private val loginUseCase: LoginUseCase,
private val userSessionManager: UserSessionManager,
private val fetchTodoUseCase: GetTodoUseCase,
private val insertTodoUseCase: SetTodoUseCase
) : ViewModel() {
private val _login = MutableStateFlow<UserLogin>()
val login get() = _login.asStateFlow()
private val _todoList = MutableStateFlow<TodoList>()
val todoList get() = _todoList.asStateFlow()
fun doLogin(email: String, password: String) { ... }
fun fetchTodoList() { ... }
fun insertTodo(todo: Todo) {
runCatching {
insertTodoUseCase.insert(todo)
.collect { isSucceed ->
if (isSucceed) {
// TODO: Show Toast
}
}
}
}
}
class FooViewModel(
private val loginUseCase: LoginUseCase,
private val userSessionManager: UserSessionManager,
private val fetchTodoUseCase: GetTodoUseCase,
private val insertTodoUseCase: SetTodoUseCase
) : ViewModel() {
private val _login = MutableStateFlow<UserLogin>()
val login get() = _login.asStateFlow()
private val _todoList = MutableStateFlow<TodoList>()
val todoList get() = _todoList.asStateFlow()
fun doLogin(email: String, password: String) { ... }
fun fetchTodoList() { ... }
fun insertTodo(todo: Todo) { ... }
}
Update
After
Framework
Login.update
UI ViewModel
ters
regi
er Todo.update
st
gi
re
ObservableUpdateFactor
y
class FooViewModel(
private val loginUseCase: LoginUseCase,
private val userSessionManager: UserSessionManager,
private val fetchTodoUseCase: GetTodoUseCase,
private val insertTodoUseCase: SetTodoUseCase
) : ViewModel() {
private val _login = MutableStateFlow<UserLogin>()
val login get() = _login.asStateFlow()
private val _todoList = MutableStateFlow<TodoList>()
val todoList get() = _todoList.asStateFlow()
fun doLogin(email: String, password: String) { ... }
fun fetchTodoList() { ... }
fun insertTodo(todo: Todo) { ... }
}
class FooViewModel(
private val login: LoginUpdate
private val fetchTodoUseCase: GetTodoUseCase,
private val insertTodoUseCase: SetTodoUseCase
) : ViewModel() {
private val _todoList = MutableStateFlow<TodoList>()
val todoList get() = _todoList.asStateFlow()
fun fetchTodoList() { ... }
fun insertTodo(todo: Todo) { ... }
}
class FooViewModel(
private val login: LoginUpdate
private val fetchTodoUseCase: GetTodoUseCase,
private val insertTodoUseCase: SetTodoUseCase
) : ViewModel() {
private val _todoList = MutableStateFlow<TodoList>()
val todoList get() = _todoList.asStateFlow()
fun fetchTodoList() { ... }
fun insertTodo(todo: Todo) { ... }
}
class FooViewModel(
private val login: LoginUpdate
private val fetchTodoUseCase: GetTodoUseCase,
private val insertTodoUseCase: SetTodoUseCase
) : ViewModel() {
private val _todoList = MutableStateFlow<TodoList>()
val todoList get() = _todoList.asStateFlow()
fun fetchTodoList() { ... }
fun insertTodo(todo: Todo) { ... }
}
class FooViewModel(
private val login: LoginUpdate
private val todo: TodoUpdate
) : ViewModel()
class FooViewModel(
private val login: LoginUpdate,
private val todo: TodoUpdate,
) : ViewModel()
class FooViewModel(
private val login: LoginUpdate,
private val todo: TodoUpdate,
) : ViewModel() {
private val factory = ObservableUpdateFactory(
eventHandler = eventHandler,
effectHandler = effectHandler,
viewModel = this
)
}
class FooViewModel(
private val login: LoginUpdate,
private val todo: TodoUpdate,
) : ViewModel() {
private val factory = ObservableUpdateFactory(
eventHandler = eventHandler,
effectHandler = effectHandler,
viewModel = this
)
init {
factory.registerUpdates(
login,
todo
)
factory.loop()
}
}
class FooViewModel(
private val login: LoginUpdate,
private val todo: TodoUpdate,
) : ViewModel() {
private val factory = ObservableUpdateFactory(
eventHandler = eventHandler,
effectHandler = effectHandler,
viewModel = this
)
init {
factory.registerUpdates(
login,
todo
)
factory.loop()
}
}
class FooViewModel(
private val login: LoginUpdate,
private val todo: TodoUpdate,
) : ViewModel() {
val state: StateFlow<FooUiState>
get() = combine(login.state, todo.state) { loginState, todoState ->
FooUiState(loginState, todoState)
}
.flowOn(dispatcher.default)
.stateIn(viewModelScope)
private val factory = ObservableUpdateFactory(
eventHandler = eventHandler,
effectHandler = effectHandler,
viewModel = this
)
init {
factory.registerUpdates(
login,
todo
)
factory.loop()
}
}
class FooViewModel(
private val login: LoginUpdate,
private val todo: TodoUpdate,
) : ViewModel() {
val state:
val state:StateFlow<FooUiState>
StateFlow<FooUiState>
get()
get()==combine(login.state,
combine(login.state,todo.state)
todo.state)
{ loginState,
{ loginState,
todoState
todoState
-> ->
FooUiState(loginState,
FooUiState(loginState,todoState)
todoState)
}}
.flowOn(dispatcher.default)
.flowOn(dispatcher.default)
.stateIn(viewModelScope)
.stateIn(viewModelScope)
private val factory = ObservableUpdateFactory(
eventHandler = eventHandler,
effectHandler = effectHandler,
viewModel = this
)
init {
factory.registerUpdates(
factory.registerUpdates(
login,
login,
todo
todo
))
factory.loop()
}
}
Update
Framework
Login.update
UI ViewModel
ters
regi
er Todo.update
st
gi
re
ObservableUpdateFactor
y
Update Scal
Framework e!
Login.update
UI ViewModel
ters
regi
er Todo.update
st
gi
re
ObservableUpdateFactor regist
e r
y ….update
Login.updat
e
First
Define your UiState / Model as respectively
LoginUiState.kt
data class LoginUiState(
val isLoggingIn: Boolean,
val authToken: String,
)
Login.updat
e
Second
Define Event and Effect that Update has
Login.action.kt
// Events
data class LoginClicked(email: String, password: String): Event
// Effects
data object NavigateToHome : Effect
data class ShowErrorMessage(message: String) : Effect
Login.updat
e
Lastly
Create your own Update!
Login.update.kt
interface LoginUpdate : Update {
val state: Flow<LoginUiState>
}
Login.update.kt
class LoginUpdateImpl(
private val loginUseCase: LoginUseCase,
private val userSessionManager: UserSessionManager
) : LoginUpdate {
private val _state = MutableStateFlow<UserLogin>()
override val state get() = _state.asStateFlow()
override fun handleEvent(event: Event) {
when (event) {
is LoginClicked -> doLogin(event.email, event.password)
}
}
private fun doLogin(email: String, password: String) {
launch {
loginUseCase
.request(email, password)
.collect { result ->
userSessionManager.set(result)
_state.update { result }
}
}
}
}
Login.update.kt
class LoginUpdateImpl(
private val loginUseCase: LoginUseCase,
private val userSessionManager: UserSessionManager
) : LoginUpdate {
private val _state = MutableStateFlow<UserLogin>()
override val state get() = _state.asStateFlow()
override fun handleEvent(event: Event) {
when (event) {
is LoginClicked -> doLogin(event.email, event.password)
}
}
private fun doLogin(email: String, password: String) {
launch {
loginUseCase
.request(email, password)
.collect { result ->
userSessionManager.set(result)
_state.update { result }
}
}
}
}
Login.update.kt
class LoginUpdateImpl(
private val loginUseCase: LoginUseCase,
private val userSessionManager: UserSessionManager
) : LoginUpdate {
override fun handleEvent(event: Event) { ... }
override fun handleEffect(effect: Effect) {
when (effect) {
is ShowErrorMessage -> {
globalEffect.send(Toast("Fail to login"))
}
}
}
private fun doLogin(email: String, password: String) {
launch {
loginUseCase
.request(email, password)
.onError { handleEffect(ShowErrorMessage(it)) }
.collect { ... }
}
}
}
Login.update.kt
class LoginUpdateImpl(
private val loginUseCase: LoginUseCase,
private val userSessionManager: UserSessionManager
) : LoginUpdate {
override fun handleEvent(event: Event) { ... }
override fun handleEffect(effect: Effect) {
when (effect) {
is ShowErrorMessage -> {
globalEffect.send(Toast("Fail to login"))
}
}
}
private fun doLogin(email: String, password: String) {
launch {
loginUseCase
.request(email, password)
.onError { handleEffect(ShowErrorMessage(it)) }
.collect { ... }
}
}
}
Login.update.kt
class LoginUpdateImpl(
private val loginUseCase: LoginUseCase,
private val userSessionManager: UserSessionManager
) : LoginUpdate {
override fun handleEvent(event: Event) { ... }
override fun handleEffect(effect: Effect) {
when (effect) {
is ShowErrorMessage -> {
globalEffect.send(Toast("Fail to login"))
}
}
}
private fun doLogin(email: String, password: String) {
launch {
loginUseCase
.request(email, password)
.onError { handleEffect(ShowErrorMessage(it)) }
.collect { ... }
}
}
}
Make Test great again, Unit Test!
✅
Unit
Test
Test your Login.update.kt
class LoginUpdateTest {
@Test
fun doCorrectLoginNavigateToHome() {
// Given
val expected = UserLogin(isSucceed="true", token="foo-bar")
val (email, password) = Pair(
"[email protected]",
"buttagowa"
)
when(loginUseCase.request(email, password)).return(expected)
test<LoginUpdate> {
event(LoginClicked(email, password))
equals(state.isLoggingIn == expected.isSucceed)
isEffect<NavigateToHome>()
}
}
}
Conclusion
TL;DR
- The architecture must be scalable.
- We are in an event-driven era, reflecting user needs.
- The Update Framework reduces complexity while ensuring
scalability (re: extensible).
Quick Demo
Thanks! barakallah fiik
@isfaaghyth
GDG Makassar