Home > Blog > tech

Mobile App Architecture คืออะไร? สอนออกแบบสถาปัตยกรรม App สำหรับ Mobile Developer 2026

mobile app architecture best practices
Mobile App Architecture Best Practices 2026
2026-04-10 | tech | 3400 words

การพัฒนา Mobile App ในปี 2026 ไม่ใช่แค่เรื่องของการเขียน Code ให้ทำงานได้อีกต่อไป แต่เป็นเรื่องของ Architecture ที่ดี ซึ่งเป็นรากฐานที่ตัดสินว่าแอปของคุณจะ Scale ได้หรือไม่ จะ Maintain ง่ายหรือยาก จะ Test ได้สะดวกหรือเปล่า และท้ายที่สุดจะส่งมอบประสบการณ์ที่ดีให้ผู้ใช้ได้หรือไม่

บทความนี้จะพาคุณเจาะลึก Mobile App Architecture ตั้งแต่พื้นฐานจนถึงระดับ Enterprise ครอบคลุมทุก Pattern ที่ Mobile Developer ต้องรู้ ทั้ง MVC, MVVM, MVP, Clean Architecture, VIPER รวมถึง State Management, Data Layer, Dependency Injection, Testing Architecture และ CI/CD Pipeline ที่เป็นมาตรฐานอุตสาหกรรม สำหรับผู้ที่สนใจพัฒนา Cross-platform ด้วย Flutter แนะนำอ่าน คู่มือ Flutter และ Dart หรือ คู่มือ React Native ประกอบด้วย

ทำไม Architecture ถึงสำคัญสำหรับ Mobile App?

หลายคนเริ่มเขียน Mobile App โดยไม่คิดเรื่อง Architecture มาก่อน เขียน Code ทั้งหมดไว้ใน Activity หรือ ViewController เดียว พอแอปเริ่มซับซ้อนขึ้น Code เริ่มพันกันจนแก้บั๊กที่หนึ่งแล้วพังอีกที่หนึ่ง ปัญหานี้เรียกว่า Massive View Controller หรือ God Activity ซึ่งเป็นสัญญาณว่าแอปขาด Architecture ที่ดี

Architecture ที่ดีช่วยให้คุณ:

Architecture Patterns สำหรับ Mobile App

MVC (Model-View-Controller)

MVC เป็น Pattern เก่าแก่ที่สุดและเป็นพื้นฐานของ iOS Development (UIKit) แบ่งออกเป็น 3 ส่วน: Model เก็บข้อมูลและ Business Logic, View แสดงผลบนหน้าจอ, Controller เป็นตัวกลางรับ Input จาก User แล้วสั่ง Model และ Update View

// MVC ใน iOS (UIKit) — ตัวอย่าง
class UserModel {
    var name: String
    var email: String

    func validate() -> Bool {
        return !name.isEmpty && email.contains("@")
    }
}

class UserViewController: UIViewController {
    @IBOutlet var nameLabel: UILabel!
    @IBOutlet var emailLabel: UILabel!

    var user: UserModel!

    override func viewDidLoad() {
        super.viewDidLoad()
        updateUI()
    }

    func updateUI() {
        nameLabel.text = user.name
        emailLabel.text = user.email
    }

    @IBAction func saveTapped() {
        if user.validate() {
            // Save to database
        }
    }
}

ข้อดี: เข้าใจง่าย เหมาะกับแอปเล็กๆ Apple ใช้เป็น Pattern หลักใน UIKit
ข้อเสีย: Controller มักจะบวมมาก (Massive View Controller) เพราะ Logic ทุกอย่างรวมอยู่ที่เดียว ทำให้ Test ยาก

MVP (Model-View-Presenter)

MVP แก้ปัญหาของ MVC โดยแยก Logic ออกจาก View ไปไว้ใน Presenter ที่เป็น Plain Object (ไม่ขึ้นกับ UI Framework) ทำให้ Test ได้ง่ายขึ้นมาก

// MVP ใน Android (Kotlin)
// Contract กำหนด Interface
interface UserContract {
    interface View {
        fun showUser(name: String, email: String)
        fun showError(message: String)
        fun showLoading()
        fun hideLoading()
    }

    interface Presenter {
        fun loadUser(userId: String)
        fun saveUser(name: String, email: String)
        fun onDestroy()
    }
}

// Presenter — ไม่มี Android dependency
class UserPresenter(
    private var view: UserContract.View?,
    private val repository: UserRepository
) : UserContract.Presenter {

    override fun loadUser(userId: String) {
        view?.showLoading()
        repository.getUser(userId) { user, error ->
            view?.hideLoading()
            if (error != null) {
                view?.showError(error.message ?: "Unknown error")
            } else if (user != null) {
                view?.showUser(user.name, user.email)
            }
        }
    }

    override fun saveUser(name: String, email: String) {
        if (name.isBlank() || !email.contains("@")) {
            view?.showError("Invalid input")
            return
        }
        repository.saveUser(User(name, email))
    }

    override fun onDestroy() {
        view = null  // ป้องกัน Memory Leak
    }
}

// Activity ทำหน้าที่เป็น View เท่านั้น
class UserActivity : AppCompatActivity(), UserContract.View {
    private lateinit var presenter: UserPresenter

    override fun showUser(name: String, email: String) {
        nameText.text = name
        emailText.text = email
    }

    override fun showError(message: String) {
        Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
    }

    override fun showLoading() { progressBar.visibility = View.VISIBLE }
    override fun hideLoading() { progressBar.visibility = View.GONE }
}

ข้อดี: Presenter ไม่ขึ้นกับ Framework จึง Test ได้ง่ายมาก View บางลง
ข้อเสีย: ต้องเขียน Interface เยอะ Presenter อาจบวมถ้าหน้าจอซับซ้อน

MVVM (Model-View-ViewModel)

MVVM เป็น Pattern ที่ได้รับความนิยมมากที่สุดในปี 2026 ทั้ง Android (Jetpack ViewModel), iOS (SwiftUI + ObservableObject) และ Cross-platform (Flutter, React Native) จุดเด่นคือ Data Binding ที่ทำให้ View อัพเดตอัตโนมัติเมื่อข้อมูลเปลี่ยน

// MVVM ใน Android (Kotlin + Jetpack)
class UserViewModel(
    private val repository: UserRepository
) : ViewModel() {

    private val _user = MutableLiveData<User>()
    val user: LiveData<User> = _user

    private val _loading = MutableLiveData<Boolean>()
    val loading: LiveData<Boolean> = _loading

    private val _error = MutableLiveData<String?>()
    val error: LiveData<String?> = _error

    fun loadUser(userId: String) {
        viewModelScope.launch {
            _loading.value = true
            try {
                val result = repository.getUser(userId)
                _user.value = result
            } catch (e: Exception) {
                _error.value = e.message
            } finally {
                _loading.value = false
            }
        }
    }

    fun saveUser(name: String, email: String) {
        if (name.isBlank() || !email.contains("@")) {
            _error.value = "Invalid input"
            return
        }
        viewModelScope.launch {
            repository.saveUser(User(name, email))
        }
    }
}

// Jetpack Compose UI — observe ViewModel
@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
    val user by viewModel.user.observeAsState()
    val loading by viewModel.loading.observeAsState(false)

    if (loading) {
        CircularProgressIndicator()
    } else {
        user?.let {
            Text("Name: ${it.name}")
            Text("Email: ${it.email}")
        }
    }
}
// MVVM ใน Flutter (Riverpod)
class UserViewModel extends StateNotifier<AsyncValue<User>> {
  final UserRepository _repository;

  UserViewModel(this._repository) : super(const AsyncLoading()) {
    loadUser();
  }

  Future<void> loadUser() async {
    state = const AsyncLoading();
    try {
      final user = await _repository.getUser();
      state = AsyncData(user);
    } catch (e, st) {
      state = AsyncError(e, st);
    }
  }

  Future<void> saveUser(String name, String email) async {
    if (name.isEmpty || !email.contains('@')) {
      state = AsyncError('Invalid input', StackTrace.current);
      return;
    }
    await _repository.saveUser(User(name: name, email: email));
  }
}

final userProvider = StateNotifierProvider<UserViewModel, AsyncValue<User>>(
  (ref) => UserViewModel(ref.read(userRepositoryProvider)),
);

ข้อดี: Reactive Data Binding ทำให้ UI อัพเดตอัตโนมัติ ViewModel ไม่รู้จัก View จึง Test ได้ง่าย รองรับ Lifecycle ดี
ข้อเสีย: อาจมี Boilerplate เยอะ การจัดการ State ที่ซับซ้อนต้องใช้ความระมัดระวัง

Clean Architecture

Clean Architecture ถูกออกแบบโดย Robert C. Martin (Uncle Bob) เป็น Architecture ระดับ Enterprise ที่แบ่ง Code เป็นชั้นๆ (Layer) แต่ละชั้นขึ้นกันผ่าน Interface เท่านั้น โดยชั้นในสุดไม่รู้จักชั้นนอก เลย

// Clean Architecture Layers
project/
├── domain/                    # ชั้นในสุด — ไม่พึ่ง Framework ใดๆ
│   ├── entities/
│   │   └── User.kt
│   ├── usecases/
│   │   ├── GetUserUseCase.kt
│   │   └── SaveUserUseCase.kt
│   └── repositories/
│       └── UserRepository.kt  # Interface เท่านั้น
│
├── data/                      # ชั้น Data — Implement Repository
│   ├── repositories/
│   │   └── UserRepositoryImpl.kt
│   ├── remote/
│   │   ├── api/
│   │   │   └── UserApi.kt
│   │   └── dto/
│   │       └── UserDto.kt
│   └── local/
│       ├── dao/
│       │   └── UserDao.kt
│       └── entities/
│           └── UserEntity.kt
│
└── presentation/              # ชั้น UI
    ├── viewmodels/
    │   └── UserViewModel.kt
    └── screens/
        └── UserScreen.kt
// Domain Layer — Use Case
class GetUserUseCase(
    private val repository: UserRepository  // Interface
) {
    suspend operator fun invoke(userId: String): Result<User> {
        return try {
            val user = repository.getUser(userId)
            if (user.isValid()) {
                Result.success(user)
            } else {
                Result.failure(InvalidUserException())
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

// Data Layer — Repository Implementation
class UserRepositoryImpl(
    private val api: UserApi,
    private val dao: UserDao,
    private val networkChecker: NetworkChecker
) : UserRepository {

    override suspend fun getUser(userId: String): User {
        return if (networkChecker.isConnected()) {
            val dto = api.getUser(userId)
            val entity = dto.toEntity()
            dao.insertUser(entity)  // Cache locally
            entity.toDomain()
        } else {
            dao.getUser(userId)?.toDomain()
                ?: throw NoDataException()
        }
    }
}
กฎทอง Clean Architecture: Dependency ต้องชี้เข้าข้างในเสมอ (Dependency Rule) Presentation -> Domain <- Data ชั้น Domain ไม่เคย import ชั้น Data หรือ Presentation เลย ถ้าต้องการความเข้าใจเรื่อง Design Patterns เพิ่มเติม ดูที่ คู่มือ Design Patterns

VIPER (View-Interactor-Presenter-Entity-Router)

VIPER เป็น Architecture ที่แยก Concern ละเอียดที่สุด นิยมใช้ใน iOS Development สำหรับแอปขนาดใหญ่

// VIPER Components
// View      — แสดง UI (UIViewController / SwiftUI View)
// Interactor — Business Logic (คล้าย Use Case)
// Presenter — จัดเตรียมข้อมูลให้ View
// Entity    — Data Model
// Router    — จัดการ Navigation

protocol UserViewProtocol: AnyObject {
    func showUser(_ viewModel: UserViewModel)
    func showError(_ message: String)
}

protocol UserInteractorProtocol {
    func fetchUser(id: String)
}

protocol UserPresenterProtocol {
    func viewDidLoad()
    func didFetchUser(_ user: User)
    func didFailFetchUser(_ error: Error)
}

protocol UserRouterProtocol {
    func navigateToProfile(user: User)
    static func createModule() -> UIViewController
}

// Interactor ทำ Business Logic
class UserInteractor: UserInteractorProtocol {
    weak var presenter: UserPresenterProtocol?
    var repository: UserRepository?

    func fetchUser(id: String) {
        repository?.getUser(id: id) { [weak self] result in
            switch result {
            case .success(let user):
                self?.presenter?.didFetchUser(user)
            case .failure(let error):
                self?.presenter?.didFailFetchUser(error)
            }
        }
    }
}

ข้อดี: แยก Concern ชัดเจนมาก Test ได้ทุกชิ้น เหมาะกับทีมใหญ่
ข้อเสีย: Boilerplate เยอะมาก ไม่เหมาะกับแอปเล็กหรือทีมเล็ก

เลือก Architecture ตามขนาดโปรเจกต์

ขนาดโปรเจกต์จำนวนหน้าจอทีมArchitecture แนะนำ
เล็ก (MVP/Prototype)3-5 หน้าจอ1 คนMVC หรือ MVVM อย่างง่าย
กลาง (Startup)10-20 หน้าจอ2-5 คนMVVM + Repository Pattern
ใหญ่ (Enterprise)30+ หน้าจอ5+ คนClean Architecture หรือ VIPER
Cross-platformแล้วแต่ขนาดแล้วแต่MVVM + Clean Architecture

State Management Patterns

State Management คือหัวใจของ Mobile App ที่ Reactive เพราะ UI ต้องแสดงข้อมูลที่ถูกต้องตลอดเวลา ถ้าจัดการ State ไม่ดี จะเจอบั๊กที่หาสาเหตุยากมาก เช่น UI ไม่อัพเดต ข้อมูลไม่ตรงกัน หรือแอปค้าง

React Native — Redux / Zustand / MobX

// Zustand (แนะนำ 2026 — เบาและง่ายกว่า Redux)
import { create } from 'zustand';

interface UserState {
  user: User | null;
  loading: boolean;
  error: string | null;
  fetchUser: (id: string) => Promise<void>;
  updateUser: (data: Partial<User>) => void;
}

const useUserStore = create<UserState>((set) => ({
  user: null,
  loading: false,
  error: null,

  fetchUser: async (id: string) => {
    set({ loading: true, error: null });
    try {
      const user = await api.getUser(id);
      set({ user, loading: false });
    } catch (error) {
      set({ error: error.message, loading: false });
    }
  },

  updateUser: (data) => {
    set((state) => ({
      user: state.user ? { ...state.user, ...data } : null,
    }));
  },
}));

// ใช้ใน Component
function UserProfile() {
  const { user, loading, fetchUser } = useUserStore();

  useEffect(() => {
    fetchUser('user-123');
  }, []);

  if (loading) return <ActivityIndicator />;
  return <Text>{user?.name}</Text>;
}

Flutter — Riverpod / Bloc

// Riverpod + Freezed (แนะนำ 2026)
@freezed
class UserState with _$UserState {
  const factory UserState.initial() = _Initial;
  const factory UserState.loading() = _Loading;
  const factory UserState.loaded(User user) = _Loaded;
  const factory UserState.error(String message) = _Error;
}

@riverpod
class UserNotifier extends _$UserNotifier {
  @override
  FutureOr<User> build(String userId) async {
    return ref.read(userRepositoryProvider).getUser(userId);
  }

  Future<void> updateUser(String name, String email) async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() =>
      ref.read(userRepositoryProvider).updateUser(name, email),
    );
  }
}

Android Native — ViewModel + StateFlow

// Modern Android State Management (2026)
data class UserUiState(
    val user: User? = null,
    val isLoading: Boolean = false,
    val errorMessage: String? = null
)

class UserViewModel(
    private val getUserUseCase: GetUserUseCase
) : ViewModel() {

    private val _uiState = MutableStateFlow(UserUiState())
    val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()

    fun loadUser(userId: String) {
        viewModelScope.launch {
            _uiState.update { it.copy(isLoading = true) }
            getUserUseCase(userId)
                .onSuccess { user ->
                    _uiState.update { it.copy(user = user, isLoading = false) }
                }
                .onFailure { error ->
                    _uiState.update { it.copy(
                        errorMessage = error.message,
                        isLoading = false
                    ) }
                }
        }
    }
}

Data Layer Design — Repository Pattern

Repository Pattern เป็นรูปแบบที่สำคัญที่สุดใน Data Layer ทำหน้าที่เป็นตัวกลางระหว่าง Business Logic และแหล่งข้อมูล (API, Database, Cache) ทำให้ Business Logic ไม่จำเป็นต้องรู้ว่าข้อมูลมาจากไหน

Offline-First Architecture

// Offline-First Repository (Kotlin)
class ProductRepositoryImpl(
    private val api: ProductApi,
    private val dao: ProductDao,
    private val connectivity: ConnectivityManager
) : ProductRepository {

    override fun getProducts(): Flow<List<Product>> {
        return dao.observeAllProducts()  // ดึงจาก Local DB ก่อนเสมอ
            .onStart {
                refreshFromNetwork()  // แล้วค่อย Sync กับ Server
            }
    }

    private suspend fun refreshFromNetwork() {
        if (!connectivity.isConnected()) return
        try {
            val remoteProducts = api.getProducts()
            dao.upsertAll(remoteProducts.map { it.toEntity() })
        } catch (e: Exception) {
            // ไม่ throw — แค่ใช้ข้อมูล Local
            Log.w("ProductRepo", "Network sync failed", e)
        }
    }

    override suspend fun syncPendingChanges() {
        val pending = dao.getPendingChanges()
        pending.forEach { change ->
            try {
                when (change.action) {
                    "CREATE" -> api.createProduct(change.toDto())
                    "UPDATE" -> api.updateProduct(change.id, change.toDto())
                    "DELETE" -> api.deleteProduct(change.id)
                }
                dao.markSynced(change.id)
            } catch (e: Exception) {
                // Retry later
            }
        }
    }
}
Offline-First คือมาตรฐาน: ในปี 2026 ผู้ใช้คาดหวังว่าแอปจะทำงานได้แม้ไม่มี Internet อ่านเรื่อง Caching Strategies เพิ่มเติมเพื่อเข้าใจหลักการ Cache

Networking Layer

Networking Layer ที่ดีต้องจัดการ API Call, Error Handling, Retry, Caching และ Authentication ให้เรียบร้อยในที่เดียว เพื่อไม่ต้องเขียนซ้ำทุกครั้ง

Retrofit (Android) / Dio (Flutter) / Axios (React Native)

// Retrofit + OkHttp (Android)
interface ProductApi {
    @GET("products")
    suspend fun getProducts(): List<ProductDto>

    @GET("products/{id}")
    suspend fun getProduct(@Path("id") id: String): ProductDto

    @POST("products")
    suspend fun createProduct(@Body product: ProductDto): ProductDto
}

// OkHttp Interceptors สำหรับ Auth, Logging, Retry
val client = OkHttpClient.Builder()
    .addInterceptor(AuthInterceptor(tokenManager))
    .addInterceptor(HttpLoggingInterceptor().apply {
        level = HttpLoggingInterceptor.Level.BODY
    })
    .addInterceptor(RetryInterceptor(maxRetries = 3))
    .connectTimeout(30, TimeUnit.SECONDS)
    .readTimeout(30, TimeUnit.SECONDS)
    .cache(Cache(cacheDir, 10 * 1024 * 1024))  // 10MB cache
    .build()
// Dio (Flutter) — เทียบเท่า Retrofit
class ApiClient {
  late final Dio _dio;

  ApiClient(TokenManager tokenManager) {
    _dio = Dio(BaseOptions(
      baseUrl: 'https://api.example.com/v1/',
      connectTimeout: Duration(seconds: 30),
      receiveTimeout: Duration(seconds: 30),
      headers: {'Content-Type': 'application/json'},
    ));

    _dio.interceptors.addAll([
      AuthInterceptor(tokenManager),
      RetryInterceptor(maxRetries: 3),
      LogInterceptor(requestBody: true, responseBody: true),
    ]);
  }

  Future<List<Product>> getProducts() async {
    final response = await _dio.get('products');
    return (response.data as List)
        .map((json) => Product.fromJson(json))
        .toList();
  }
}

Local Storage — เลือกให้เหมาะกับงาน

เทคโนโลยีประเภทเหมาะกับPlatform
SQLite / RoomRelational DBข้อมูลโครงสร้างซับซ้อน, Query มากAndroid, iOS, Flutter
RealmObject DBข้อมูล Object, Real-time SyncAndroid, iOS, React Native
HiveKey-Value / Boxข้อมูลง่ายๆ, เร็วมากFlutter
MMKVKey-Valueแทน SharedPreferences (เร็วกว่า 100x)Android, iOS, React Native
AsyncStorageKey-Valueค่า Config, TokenReact Native
Core DataObject Graphข้อมูลซับซ้อน, MigrationiOS
Drift (Moor)SQL BuilderType-safe SQL ใน FlutterFlutter
// Room Database (Android)
@Entity(tableName = "users")
data class UserEntity(
    @PrimaryKey val id: String,
    val name: String,
    val email: String,
    @ColumnInfo(name = "updated_at") val updatedAt: Long,
    @ColumnInfo(name = "is_synced") val isSynced: Boolean = false
)

@Dao
interface UserDao {
    @Query("SELECT * FROM users ORDER BY updated_at DESC")
    fun observeAll(): Flow<List<UserEntity>>

    @Query("SELECT * FROM users WHERE id = :id")
    suspend fun getById(id: String): UserEntity?

    @Upsert
    suspend fun upsert(user: UserEntity)

    @Query("SELECT * FROM users WHERE is_synced = 0")
    suspend fun getPendingSync(): List<UserEntity>
}

Dependency Injection (DI)

Dependency Injection ทำให้โค้ดหลวมๆ (Loosely Coupled) ไม่ผูกกันแน่น สลับ Implementation ได้ง่าย โดยเฉพาะตอน Test ที่ต้องใส่ Mock แทน สำหรับพื้นฐาน DI และ Design Patterns อ่านที่ คู่มือ Design Patterns

Dagger Hilt (Android)

// Hilt Module
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

    @Provides @Singleton
    fun provideOkHttpClient(): OkHttpClient {
        return OkHttpClient.Builder()
            .addInterceptor(AuthInterceptor())
            .build()
    }

    @Provides @Singleton
    fun provideRetrofit(client: OkHttpClient): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://api.example.com/")
            .client(client)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

    @Provides @Singleton
    fun provideUserApi(retrofit: Retrofit): UserApi {
        return retrofit.create(UserApi::class.java)
    }
}

@Module
@InstallIn(SingletonComponent::class)
object RepositoryModule {

    @Provides @Singleton
    fun provideUserRepository(
        api: UserApi,
        dao: UserDao
    ): UserRepository {
        return UserRepositoryImpl(api, dao)
    }
}

// ใช้ใน ViewModel
@HiltViewModel
class UserViewModel @Inject constructor(
    private val getUserUseCase: GetUserUseCase
) : ViewModel() {
    // Hilt จะ Inject dependencies ให้อัตโนมัติ
}

get_it (Flutter)

// Service Locator ด้วย get_it
final getIt = GetIt.instance;

void setupDependencies() {
  // Singletons
  getIt.registerLazySingleton<Dio>(() => createDio());
  getIt.registerLazySingleton<UserApi>(() => UserApi(getIt()));
  getIt.registerLazySingleton<AppDatabase>(() => AppDatabase());

  // Repositories
  getIt.registerLazySingleton<UserRepository>(
    () => UserRepositoryImpl(
      api: getIt(),
      database: getIt(),
    ),
  );

  // Use Cases
  getIt.registerFactory(() => GetUserUseCase(getIt()));
  getIt.registerFactory(() => SaveUserUseCase(getIt()));
}

Navigation Patterns

Navigation ใน Mobile App มีความซับซ้อนมากกว่า Web เพราะต้องจัดการ Back Stack, Deep Link, Tab Navigation, Modal, และ Nested Navigation ให้สอดคล้องกัน

// Android — Jetpack Navigation + Type-safe Args
// nav_graph.xml
<navigation
    android:id="@+id/nav_graph"
    app:startDestination="@id/homeFragment">

    <fragment android:id="@+id/homeFragment"
        android:name=".ui.home.HomeFragment">
        <action android:id="@+id/toProfile"
            app:destination="@id/profileFragment" />
    </fragment>

    <fragment android:id="@+id/profileFragment"
        android:name=".ui.profile.ProfileFragment">
        <argument android:name="userId"
            app:argType="string" />
    </fragment>
</navigation>
// Flutter — GoRouter (Declarative)
final router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomeScreen(),
      routes: [
        GoRoute(
          path: 'profile/:userId',
          builder: (context, state) => ProfileScreen(
            userId: state.pathParameters['userId']!,
          ),
        ),
        GoRoute(
          path: 'settings',
          builder: (context, state) => const SettingsScreen(),
        ),
      ],
    ),
  ],
  redirect: (context, state) {
    final isLoggedIn = ref.read(authProvider).isLoggedIn;
    if (!isLoggedIn && state.matchedLocation != '/login') {
      return '/login';
    }
    return null;
  },
);

Modular Architecture สำหรับ App ขนาดใหญ่

เมื่อแอปมีขนาดใหญ่ขึ้น (50+ หน้าจอ, 10+ Developer) การเขียนทุกอย่างใน Module เดียวจะทำให้ Build Time ช้า Code Conflict บ่อย และยากต่อการ Maintain ทางออกคือ Modular Architecture ที่แบ่งแอปเป็น Feature Module หลายๆ อัน

// Multi-module Structure
project/
├── app/                    # Main Application Module
│   └── src/main/
│       └── App.kt          # เชื่อมทุก Module เข้าด้วยกัน
│
├── core/                   # Shared Utilities
│   ├── core-network/       # API Client, Interceptors
│   ├── core-database/      # Database Config, Base DAO
│   ├── core-ui/            # Common Widgets, Theme
│   └── core-common/        # Extensions, Utils
│
├── features/               # Feature Modules (แยกอิสระ)
│   ├── feature-auth/       # Login, Register, Forgot Password
│   ├── feature-home/       # Home Feed, Dashboard
│   ├── feature-profile/    # User Profile, Settings
│   ├── feature-chat/       # Chat, Messaging
│   └── feature-payment/    # Payment, Subscription
│
└── shared/                 # Shared Business Logic
    ├── shared-domain/      # Entities, Use Cases ที่ใช้ร่วม
    └── shared-data/        # Shared Repositories
ข้อดีของ Modular Architecture: Build Time เร็วขึ้น 3-5 เท่า (เพราะ Build เฉพาะ Module ที่เปลี่ยน), ทีมทำงานอิสระไม่ Conflict, Code Ownership ชัดเจน, สามารถ Reuse Module ข้าม Project ได้ สำหรับ Architecture ระดับ System ดูที่ คู่มือ System Design

Testing Architecture

Architecture ที่ดีต้อง Test ได้ง่าย ใน Mobile App เราแบ่ง Test เป็น 3 ระดับตาม Testing Pyramid

Unit Test — ทดสอบ Logic

// Unit Test สำหรับ Use Case (Kotlin + JUnit + MockK)
class GetUserUseCaseTest {

    private val repository: UserRepository = mockk()
    private val useCase = GetUserUseCase(repository)

    @Test
    fun `should return user when found`() = runTest {
        // Given
        val expected = User("1", "John", "john@test.com")
        coEvery { repository.getUser("1") } returns expected

        // When
        val result = useCase("1")

        // Then
        assertTrue(result.isSuccess)
        assertEquals(expected, result.getOrNull())
    }

    @Test
    fun `should return failure when user invalid`() = runTest {
        // Given
        val invalidUser = User("1", "", "")  // empty name
        coEvery { repository.getUser("1") } returns invalidUser

        // When
        val result = useCase("1")

        // Then
        assertTrue(result.isFailure)
    }
}

Widget / UI Test — ทดสอบ UI Component

// Widget Test (Flutter)
void main() {
  testWidgets('UserCard shows name and email', (tester) async {
    await tester.pumpWidget(
      MaterialApp(
        home: UserCard(
          user: User(name: 'John', email: 'john@test.com'),
        ),
      ),
    );

    expect(find.text('John'), findsOneWidget);
    expect(find.text('john@test.com'), findsOneWidget);
  });

  testWidgets('UserCard shows loading state', (tester) async {
    await tester.pumpWidget(
      MaterialApp(
        home: UserCard(isLoading: true),
      ),
    );

    expect(find.byType(CircularProgressIndicator), findsOneWidget);
  });
}

Integration / E2E Test

// Integration Test (Flutter)
void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  testWidgets('Login flow works correctly', (tester) async {
    app.main();
    await tester.pumpAndSettle();

    // กรอก Email
    await tester.enterText(find.byKey(Key('email_field')), 'test@test.com');
    // กรอก Password
    await tester.enterText(find.byKey(Key('password_field')), 'password123');
    // กด Login
    await tester.tap(find.byKey(Key('login_button')));
    await tester.pumpAndSettle();

    // ตรวจสอบว่าไปหน้า Home
    expect(find.text('Welcome'), findsOneWidget);
  });
}

สำหรับพื้นฐาน Testing เพิ่มเติม อ่านที่ คู่มือ Software Testing และเครื่องมือ E2E Testing ที่ คู่มือ Playwright

CI/CD สำหรับ Mobile App

CI/CD ใน Mobile App มีความซับซ้อนกว่า Web เพราะต้อง Build Native Binary, Sign App, จัดการ Certificates และ Upload ไป App Store / Google Play

Fastlane — มาตรฐาน CI/CD สำหรับ Mobile

# Fastfile (Ruby DSL)
default_platform(:ios)

platform :ios do
  desc "Deploy to TestFlight"
  lane :beta do
    increment_build_number
    build_app(
      scheme: "MyApp",
      workspace: "MyApp.xcworkspace",
      export_method: "app-store"
    )
    upload_to_testflight(
      skip_waiting_for_build_processing: true
    )
    slack(message: "iOS Beta deployed!")
  end

  desc "Deploy to App Store"
  lane :release do
    build_app(scheme: "MyApp")
    upload_to_app_store(
      skip_metadata: false,
      skip_screenshots: false
    )
  end
end

platform :android do
  desc "Deploy to Play Store Internal"
  lane :beta do
    gradle(task: "clean bundleRelease")
    upload_to_play_store(
      track: "internal",
      aab: "app/build/outputs/bundle/release/app-release.aab"
    )
  end
end

EAS Build (Expo / React Native)

// eas.json
{
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal"
    },
    "preview": {
      "distribution": "internal",
      "android": {
        "buildType": "apk"
      }
    },
    "production": {
      "android": {
        "buildType": "app-bundle"
      },
      "ios": {
        "autoIncrement": true
      }
    }
  },
  "submit": {
    "production": {
      "android": {
        "serviceAccountKeyPath": "./play-store-key.json",
        "track": "production"
      },
      "ios": {
        "appleId": "your@apple.com",
        "ascAppId": "1234567890"
      }
    }
  }
}
# EAS CLI Commands
eas build --platform all --profile production
eas submit --platform all --profile production
eas update --branch production --message "Bug fix v1.2.1"

สำหรับพื้นฐาน CI/CD ทั่วไป อ่านที่ คู่มือ CI/CD Pipeline

Performance Architecture

Performance ที่ดีไม่ได้เกิดจากการ Optimize ทีหลัง แต่ต้องออกแบบตั้งแต่ Architecture เลย

Lazy Loading

// Lazy Loading ใน Flutter
class ProductListScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: products.length + 1,  // +1 สำหรับ loading indicator
      itemBuilder: (context, index) {
        if (index == products.length) {
          // โหลดหน้าถัดไป
          context.read(productProvider.notifier).loadMore();
          return const CircularProgressIndicator();
        }
        return ProductCard(product: products[index]);
      },
    );
  }
}

Image Caching

// Image Caching ด้วย Coil (Android)
AsyncImage(
    model = ImageRequest.Builder(context)
        .data(product.imageUrl)
        .crossfade(true)
        .memoryCachePolicy(CachePolicy.ENABLED)
        .diskCachePolicy(CachePolicy.ENABLED)
        .build(),
    contentDescription = product.name,
    placeholder = painterResource(R.drawable.placeholder),
    error = painterResource(R.drawable.error_image)
)

// Flutter — cached_network_image
CachedNetworkImage(
  imageUrl: product.imageUrl,
  placeholder: (context, url) => const CircularProgressIndicator(),
  errorWidget: (context, url, error) => const Icon(Icons.error),
  memCacheWidth: 400,  // Resize ใน Memory
)

Pagination Pattern

// Cursor-based Pagination (ดีกว่า Offset)
class PaginatedRepository {
  Future<PaginatedResult<Product>> getProducts({
    String? cursor,
    int limit = 20,
  }) async {
    final response = await api.get('/products', queryParameters: {
      'cursor': cursor,
      'limit': limit,
    });

    return PaginatedResult(
      items: response.data['items'].map(Product.fromJson).toList(),
      nextCursor: response.data['next_cursor'],
      hasMore: response.data['has_more'],
    );
  }
}

Security Architecture

Security ต้องถูก Built-in ตั้งแต่ Architecture ไม่ใช่เพิ่มทีหลัง สำหรับเรื่อง Security เชิงลึก อ่านที่ คู่มือ Web Security และ คู่มือ OAuth2 และ JWT

Certificate Pinning

// OkHttp Certificate Pinning (Android)
val client = OkHttpClient.Builder()
    .certificatePinner(
        CertificatePinner.Builder()
            .add("api.example.com", "sha256/AAAAAAA...")
            .add("api.example.com", "sha256/BBBBBBB...")  // Backup pin
            .build()
    )
    .build()

// Flutter — Dio + SecurityContext
final securityContext = SecurityContext();
securityContext.setTrustedCertificatesBytes(certBytes);
final httpClient = HttpClient(context: securityContext);
final dio = Dio()..httpClientAdapter = IOHttpClientAdapter(
  createHttpClient: () => httpClient,
);

Secure Storage

// Android — EncryptedSharedPreferences
val masterKey = MasterKey.Builder(context)
    .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
    .build()

val securePrefs = EncryptedSharedPreferences.create(
    context, "secure_prefs", masterKey,
    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)

securePrefs.edit().putString("auth_token", token).apply()

// Flutter — flutter_secure_storage
final storage = FlutterSecureStorage();
await storage.write(key: 'auth_token', value: token);
final token = await storage.read(key: 'auth_token');

Code Obfuscation

# Android ProGuard / R8 (build.gradle)
android {
    buildTypes {
        release {
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
                'proguard-rules.pro'
        }
    }
}

# Flutter
flutter build apk --obfuscate --split-debug-info=build/debug-info

Analytics Architecture

Analytics Architecture ที่ดีต้องเป็น Abstraction Layer ไม่ผูกกับ Provider ใดๆ เพื่อให้เปลี่ยน Provider ได้ง่าย

// Analytics Abstraction Layer
abstract class AnalyticsTracker {
  void trackEvent(String name, Map<String, dynamic> params);
  void trackScreen(String screenName);
  void setUserProperty(String key, String value);
  void setUserId(String userId);
}

class FirebaseAnalyticsTracker implements AnalyticsTracker {
  final FirebaseAnalytics _analytics = FirebaseAnalytics.instance;

  @override
  void trackEvent(String name, Map<String, dynamic> params) {
    _analytics.logEvent(name: name, parameters: params);
  }

  @override
  void trackScreen(String screenName) {
    _analytics.setCurrentScreen(screenName: screenName);
  }
}

class MixpanelTracker implements AnalyticsTracker {
  final Mixpanel _mixpanel;
  MixpanelTracker(this._mixpanel);

  @override
  void trackEvent(String name, Map<String, dynamic> params) {
    _mixpanel.track(name, properties: params);
  }
}

// Composite Tracker — ส่งทั้ง Firebase + Mixpanel
class CompositeAnalyticsTracker implements AnalyticsTracker {
  final List<AnalyticsTracker> _trackers;
  CompositeAnalyticsTracker(this._trackers);

  @override
  void trackEvent(String name, Map<String, dynamic> params) {
    for (final tracker in _trackers) {
      tracker.trackEvent(name, params);
    }
  }

  @override
  void trackScreen(String screenName) {
    for (final tracker in _trackers) {
      tracker.trackScreen(screenName);
    }
  }
}

สรุป — Architecture Decision Checklist

หัวข้อตัวเลือก 2026คำแนะนำ
Architecture PatternMVVM / Clean ArchitectureMVVM สำหรับทั่วไป, Clean Architecture สำหรับ Enterprise
State ManagementRiverpod, Zustand, StateFlowเลือกตาม Framework ที่ใช้
NetworkingRetrofit, Dio, Axiosใช้ Interceptor จัดการ Auth/Retry
Local StorageRoom, Hive, MMKVRoom/Drift สำหรับ SQL, MMKV สำหรับ Key-Value
DIHilt, get_it, RiverpodHilt สำหรับ Android, get_it สำหรับ Flutter
NavigationJetpack Navigation, GoRouterDeclarative Navigation เป็นมาตรฐาน
TestingJUnit, Flutter Test, Jestเน้น Unit Test 70%, Widget 20%, E2E 10%
CI/CDFastlane, EAS, BitriseFastlane สำหรับ Native, EAS สำหรับ Expo

Architecture ไม่มีสูตรสำเร็จตายตัว แต่หลักการ Separation of Concerns, Testability และ Dependency Inversion เป็นรากฐานที่ไม่เปลี่ยน ไม่ว่าคุณจะเลือก Pattern ไหน ยึดหลักเหล่านี้ไว้แล้วแอปของคุณจะ Maintain ได้ในระยะยาว สำหรับเรื่อง Clean Code เพิ่มเติม อ่านที่ คู่มือ Clean Code

ในบทความถัดไป เราจะพูดถึง Push Notification และ Real-time สำหรับ Mobile App ซึ่งเป็นอีกหนึ่งหัวข้อสำคัญที่ต้องออกแบบ Architecture ให้ดีตั้งแต่แรก


Back to Blog | iCafe Forex | SiamLanCard | Siam2R