Mobile não quebra quando você tem 1.000 usuários. Ele quebra quando você tem 1 milhão.
E não é por causa da UI. É por causa da arquitetura.
Se você ainda organiza seu app por “telas” ou “pastas de features soltas”, cedo ou tarde vai enfrentar:
- código impossível de manter
- bugs que ninguém sabe de onde vêm
- deploys com medo
- refactors que nunca acontecem
Sênior não constrói tela. Sênior constrói sistema evolutivo.
O problema: apps mobile viram monólitos silenciosos
No começo, tudo parece simples:
/screens
HomeScreen.tsx
ProfileScreen.tsx
/services
api.ts
/utils
helpers.ts
Aí o app cresce.
E de repente:
api.tstem 2.000 linhashelpers.tsvirou um Frankenstein- lógica de negócio tá misturada com UI
- ninguém sabe o impacto de uma mudança
Você criou um monólito.
Só que pior: um monólito sem disciplina de backend.
Clean Architecture no mobile (de verdade, não de slide)
A ideia é simples:
Separar o que o app faz de como ele faz.
Estrutura base:
/domain
/entities
/usecases
/data
/repositories
/datasources
/presentation
/ui
/viewmodels
Agora vamos ver isso na prática.
Exemplo: caso de uso
class GetUserProfileUseCase(
private val repository: UserRepository
) {
suspend operator fun invoke(userId: String): User {
return repository.getUser(userId)
}
}
Aqui não tem Retrofit, não tem JSON, não tem UI.
Só regra de negócio.
Repository (camada de abstração)
interface UserRepository {
suspend fun getUser(userId: String): User
}
Implementação (data layer)
class UserRepositoryImpl(
private val api: UserApi
) : UserRepository {
override suspend fun getUser(userId: String): User {
val response = api.getUser(userId)
return response.toDomain()
}
}
UI consumindo isso
class ProfileViewModel(
private val getUserProfile: GetUserProfileUseCase
) : ViewModel() {
val state = MutableStateFlow<User?>(null)
fun load(userId: String) {
viewModelScope.launch {
state.value = getUserProfile(userId)
}
}
}
👉 Resultado:
- UI desacoplada
- regra de negócio testável
- troca de API sem quebrar tudo
Modularização: quando um app vira um ecossistema
Quando seu app cresce, um único módulo vira gargalo.
A solução: feature modules.
/features
/home
/profile
/checkout
/core
/network
/designsystem
/utils
Cada feature tem:
- UI
- ViewModel
- UseCases
- Repositórios próprios
Exemplo de estrutura de feature
/features/profile
ProfileScreen.kt
ProfileViewModel.kt
GetUserProfileUseCase.kt
ProfileRepository.kt
👉 Benefícios reais:
- times trabalham em paralelo
- builds mais rápidos
- isolamento de bugs
- deploy mais previsível
Gerenciamento de estado: o caos invisível
Sem estratégia de estado, seu app vira isso:
- loading duplicado
- dados inconsistentes
- telas com comportamentos estranhos
Exemplo com Redux (React Native)
const initialState = {
user: null,
loading: false,
error: null
}
function reducer(state = initialState, action) {
switch (action.type) {
case 'FETCH_USER':
return { ...state, loading: true }
case 'FETCH_USER_SUCCESS':
return { ...state, loading: false, user: action.payload }
case 'FETCH_USER_ERROR':
return { ...state, loading: false, error: action.error }
default:
return state
}
}
Dispatch com async
export const fetchUser = (id) => async (dispatch) => {
dispatch({ type: 'FETCH_USER' })
try {
const data = await api.getUser(id)
dispatch({ type: 'FETCH_USER_SUCCESS', payload: data })
} catch (e) {
dispatch({ type: 'FETCH_USER_ERROR', error: e })
}
}
👉 Sênior não escolhe ferramenta.
Escolhe previsibilidade de estado.
Offline-first: o mundo real não tem Wi-Fi perfeito
Se seu app depende 100% da API, ele já nasceu frágil.
Estratégia: cache + sincronização.
Exemplo simples (Kotlin + Room)
suspend fun getUser(userId: String): User {
val cached = database.userDao().getUser(userId)
return try {
val remote = api.getUser(userId)
database.userDao().insert(remote.toEntity())
remote.toDomain()
} catch (e: Exception) {
cached.toDomain()
}
}
👉 Isso aqui muda o jogo:
- app funciona offline
- UX muito melhor
- menos dependência de backend
🔁 Sincronização inteligente
Offline-first não é só cache.
É resolver conflitos.
Exemplo de sync (pseudo-código)
fun syncData(local: List<Item>, remote: List<Item>): List<Item> {
return resolveConflicts(local, remote)
}
Estratégias reais:
- last write wins
- versionamento
- merge de dados
Monorepo vs Multirepo (decisão de time, não de código)
Monorepo
- tudo junto
- fácil compartilhar código
- builds mais pesados
Multirepo
- isolamento total
- mais controle
- overhead de integração
👉 Sênior pensa assim:
“Isso aqui escala com o time ou só com o código?”
O ponto que separa sênior de pleno
Pleno resolve feature.
Sênior resolve complexidade ao longo do tempo.
Arquitetura mobile em escala não é sobre:
- usar Clean Architecture porque tá na moda
- dividir pasta bonitinha
- escolher Redux ou Bloc
É sobre:
- reduzir acoplamento
- permitir evolução contínua
- evitar reescrita total em 1 ano
Conclusão
Se seu app:
- cresce rápido
- tem múltiplos devs
- depende de backend
- precisa evoluir constantemente
Então arquitetura não é luxo.
É sobrevivência.




