1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Androidアーキテクチャの15年史:Fat Activityからクリーンアーキテクチャまでの激動の歩み

Posted at

はじめに

こんにちは!Android開発歴もう10年以上になる僕ですが、振り返ってみると「アーキテクチャの変遷」が一番印象深いなと感じています😊

Android開発を始めて何年か経つと、必ずぶつかるのが「アーキテクチャ問題」ですよね。最初は単純なActivityにすべてを書いていたものの、アプリが複雑になるにつれて「なんかコードがぐちゃぐちゃになってきた...😅」という経験、皆さんも一度はあるのではないでしょうか?

僕もその一人で、過去に保守性の悪いコードで本当に痛い目を見た経験があります。「なんでこんなコード書いたんだ過去の自分...」と頭を抱えたことも一度や二度じゃありません😭

そこで今回は、Android開発の歴史とともに進化してきたアーキテクチャパターンを振り返り、なぜ現在のようなパターンが推奨されているのかを、実体験を交えながら整理してみたいと思います。

この記事では、Android初期の「Fat Activity時代」から、現在主流の「MVVM + Clean Architecture + Jetpack Compose」まで、実際のコード例と共に比較していきます。きっと「あの時代のコード、懐かしいな...」と思っていただけるはずです🚀

Android開発の歴史とアーキテクチャの変遷

さて、ここからが本題です!僕が実際に体験してきたAndroidアーキテクチャの変遷を、時代ごとに振り返ってみましょう。

第1期: Fat Activity時代(2008-2012年)

Android開発の黎明期は、まさに「何でもActivityに詰め込む」時代でした。当時はアーキテクチャパターンという概念自体がほとんど意識されておらず、Googleの公式サンプルコードでさえUI、ビジネスロジック、データアクセスがActivityに混在していました。端末のスペックも限られており、「とにかく動くものを作る」ことが最優先だったこの時代は、Android開発の原点とも言える時期です。

時代背景

  • Android 1.0リリース(2008年)
  • 限られたリソース環境での開発 - 端末スペックが今と全然違った😅
  • XMLベースの静的UI
  • AsyncTask - 今は非推奨になったやつ
  • SQLiteOpenHelper - 生SQL書いてました
  • HttpUrlConnection - REST APIなんて概念もまだまだ
public class MainActivity extends Activity {
    private TextView textView;
    private Button button;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        textView = findViewById(R.id.textView);
        button = findViewById(R.id.button);
        
        // すべてのロジックがActivityに集約
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // ネットワーク処理
                new AsyncTask<Void, Void, String>() {
                    @Override
                    protected String doInBackground(Void... voids) {
                        // API呼び出し処理
                        return fetchDataFromServer();
                    }
                    
                    @Override
                    protected void onPostExecute(String result) {
                        textView.setText(result);
                    }
                }.execute();
                
                // データベース処理
                SQLiteDatabase db = openOrCreateDatabase("MyDB", MODE_PRIVATE, null);
                // SQL処理...
                
                // UI更新処理
                // その他もろもろ...
            }
        });
    }
    
    private String fetchDataFromServer() {
        // HTTPクライアント処理
        return "データ";
    }
}

問題点

  • Fat Activity: すべての処理がActivityに集約されて可読性が悪化
  • テストの困難さ: UIとロジックが密結合でユニットテストが困難
  • メモリリーク: AsyncTaskの不適切な使用でアプリがクラッシュ
  • 再利用性の低さ: 他の画面で同じロジックを使えない

「動けばいいでしょ!」...今考えると恐ろしいコードを書いてました😂

第2期: MVP全盛期(2013-2016年)

Fat Activityの問題点が明確になり、Android開発コミュニティで初めて本格的に「アーキテクチャ」が議論され始めた時代です。MVPパターンの導入により、UIロジックとビジネスロジックを明確に分離できるようになり、ユニットテストが現実的になりました。RxJavaやRetrofitといった強力なライブラリの登場も相まって、Android開発は「なんとなく動く」から「設計されたコード」へと進化していきました。

時代背景

  • Google I/O 2014-2015でテスタビリティの重要性が強調
  • RxJavaの登場(2014年)
  • Retrofitの普及(2013年) - HTTPクライアントがようやく楽に!
  • Dagger 2 - DIの概念を初体験
  • Butterknife - findViewById地獄から解放!
// View Interface
public interface MainView {
    void showLoading();
    void hideLoading();
    void showData(List<Item> items);
    void showError(String message);
}

// Presenter
public class MainPresenter {
    private MainView view;
    private DataRepository repository;
    
    public MainPresenter(MainView view, DataRepository repository) {
        this.view = view;
        this.repository = repository;
    }
    
    public void loadData() {
        view.showLoading();
        repository.getData()
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(
                items -> {
                    view.hideLoading();
                    view.showData(items);
                },
                error -> {
                    view.hideLoading();
                    view.showError(error.getMessage());
                }
            );
    }
}

// Activity (View)
public class MainActivity extends AppCompatActivity implements MainView {
    private MainPresenter presenter;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        presenter = new MainPresenter(this, new DataRepository());
        presenter.loadData();
    }
    
    @Override
    public void showData(List<Item> items) {
        // RecyclerViewの更新など
    }
    
    @Override
    public void showLoading() {
        progressBar.setVisibility(View.VISIBLE);
    }
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
        presenter.destroy(); // メモリリーク対策
    }
}

改善点

  • 責任の分離: UIロジックとビジネスロジックの分離
  • テスト容易性: Presenterのユニットテストが可能
  • 再利用性: Presenterを他の画面でも使用可能

問題点

  • Fat Presenter: Model層が薄いとPresenterが肥大化 - 結局同じ問題😅
  • ライフサイクル管理: Android固有の複雑なライフサイクルに対応が必要
  • 多くのインターフェース: View interfaceの定義が煩雑
  • メモリリーク: サブスクリプションの管理漏れ - これが一番辛かった😭

MVPは良かったけど、インターフェース地獄で疲れました...

第3期: MVVM + Architecture Components時代(2017-2019年)

GoogleがついにAndroid開発の公式アーキテクチャガイドラインを発表し、Architecture Componentsという強力なツールセットを提供した時代です。ViewModelとLiveDataの組み合わせにより、Androidの複雑なライフサイクル管理が大幅に簡素化されました。MVPで悩まされていたメモリリークやライフサイクル管理の問題が、フレームワークレベルで解決され、開発者はビジネスロジックに集中できるようになりました。この時期からAndroid開発は「Googleの推奨パターン」が明確になり、チーム開発での標準化が進みました。

時代背景

  • Google I/O 2017でAndroid Architecture Components発表
  • ライフサイクルを考慮したコンポーネントの標準化
  • LiveDataによるリアクティブプログラミング

Android Architecture Componentsの構成

  • ViewModel: UI関連データの保持
  • LiveData: ライフサイクルを考慮した監視可能データ
  • Room: SQLiteのORM
  • Lifecycle: ライフサイクル管理の標準化

MVVM実装例

// ViewModel
public class UserViewModel extends ViewModel {
    private MutableLiveData<List<User>> users = new MutableLiveData<>();
    private MutableLiveData<Boolean> loading = new MutableLiveData<>();
    private UserRepository repository;
    
    public UserViewModel(UserRepository repository) {
        this.repository = repository;
    }
    
    public LiveData<List<User>> getUsers() {
        return users;
    }
    
    public LiveData<Boolean> getLoading() {
        return loading;
    }
    
    public void loadUsers() {
        loading.setValue(true);
        repository.getUsers().observeForever(userList -> {
            loading.setValue(false);
            users.setValue(userList);
        });
    }
    
    @Override
    protected void onCleared() {
        super.onCleared();
        // リソースのクリーンアップ
    }
}

// Repository
public class UserRepository {
    private UserDao userDao;
    private UserApiService apiService;
    
    public LiveData<List<User>> getUsers() {
        // Room + Retrofit を使ったNetworkBoundResource実装
        return new NetworkBoundResource<List<User>, List<UserResponse>>() {
            @Override
            protected void saveCallResult(@NonNull List<UserResponse> item) {
                userDao.insertUsers(item);
            }
            
            @Override
            protected LiveData<List<User>> loadFromDb() {
                return userDao.getAllUsers();
            }
            
            @Override
            protected LiveData<ApiResponse<List<UserResponse>>> createCall() {
                return apiService.getUsers();
            }
        }.asLiveData();
    }
}

// Activity
public class UserActivity extends AppCompatActivity {
    private UserViewModel viewModel;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_user);
        
        viewModel = ViewModelProviders.of(this).get(UserViewModel.class);
        
        viewModel.getUsers().observe(this, users -> {
            if (users != null) {
                updateUI(users);
            }
        });
        
        viewModel.getLoading().observe(this, isLoading -> {
            progressBar.setVisibility(isLoading ? View.VISIBLE : View.GONE);
        });
        
        viewModel.loadUsers();
    }
}

Data Binding活用例

<!-- activity_user.xml -->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="viewmodel"
            type="com.example.UserViewModel" />
    </data>
    
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        
        <ProgressBar
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:visibility="@{viewmodel.loading ? View.VISIBLE : View.GONE}" />
            
        <androidx.recyclerview.widget.RecyclerView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:items="@{viewmodel.users}" />
            
    </LinearLayout>
</layout>

改善点

  • ライフサイクル管理: 自動的なサブスクリプション管理
  • 構成変更への対応: ViewModelが画面回転時に生存
  • 宣言的UI: Data Bindingによる宣言的UI記述
  • テスト改善: ViewModelの単体テストが容易

問題点

  • Data Bindingの複雑さ: XMLでのロジック記述が複雑化
  • LiveData制約: エラーハンドリングが困難
  • XML肥大化: レイアウトファイルの可読性低下

第4期: Clean Architecture導入期(2018-2020年)

アプリの規模と複雑さが増大し、MVVMだけでは保守性やスケーラビリティに限界を感じる開発チームが増えてきた時代です。Uncle Bobが提唱したClean Architectureの概念がAndroid開発に本格導入され、Presentation層、Domain層、Data層という明確な責任分離が標準となりました。UseCaseパターンやRepository パターンの採用により、ビジネスロジックの再利用性とテスタビリティが劇的に向上し、大規模チーム開発でも一貫性のあるコード品質を維持できるようになりました。

時代背景

  • Uncle Bob's Clean Architecture(2017年)の影響
  • 大規模開発での保守性の重要性認識
  • チーム開発でのスケーラビリティ要求

Clean Architectureの層構成

Presentation Layer (UI)
├── Activity/Fragment
├── ViewModel
└── UI State

Domain Layer (Business Logic)
├── UseCase/Interactor
├── Repository Interface
└── Domain Models

Data Layer
├── Repository Implementation
├── Data Source (API, Database)
└── Data Models

実装例

// Domain Layer - UseCase
public class GetUserListUseCase {
    private UserRepository userRepository;
    
    @Inject
    public GetUserListUseCase(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    public LiveData<Resource<List<User>>> execute() {
        return userRepository.getUserList();
    }
}

// Domain Layer - Repository Interface
public interface UserRepository {
    LiveData<Resource<List<User>>> getUserList();
    LiveData<Resource<User>> getUser(String userId);
}

// Data Layer - Repository Implementation
public class UserRepositoryImpl implements UserRepository {
    private UserRemoteDataSource remoteDataSource;
    private UserLocalDataSource localDataSource;
    
    @Inject
    public UserRepositoryImpl(
        UserRemoteDataSource remoteDataSource,
        UserLocalDataSource localDataSource) {
        this.remoteDataSource = remoteDataSource;
        this.localDataSource = localDataSource;
    }
    
    @Override
    public LiveData<Resource<List<User>>> getUserList() {
        return new NetworkBoundResource<List<User>, List<UserResponse>>() {
            @Override
            protected void saveCallResult(List<UserResponse> item) {
                localDataSource.insertUsers(UserMapper.mapToEntity(item));
            }
            
            @Override
            protected LiveData<List<User>> loadFromDb() {
                return localDataSource.getAllUsers();
            }
            
            @Override
            protected LiveData<ApiResponse<List<UserResponse>>> createCall() {
                return remoteDataSource.getUsers();
            }
            
            @Override
            protected boolean shouldFetch(List<User> data) {
                return data == null || data.isEmpty() || 
                       shouldRefresh(data.get(0).lastUpdated);
            }
        }.asLiveData();
    }
}

// Presentation Layer - ViewModel
public class UserListViewModel extends ViewModel {
    private GetUserListUseCase getUserListUseCase;
    
    @Inject
    public UserListViewModel(GetUserListUseCase getUserListUseCase) {
        this.getUserListUseCase = getUserListUseCase;
    }
    
    public LiveData<Resource<List<User>>> getUsers() {
        return getUserListUseCase.execute();
    }
}

Hiltによる依存性注入

@Module
@InstallIn(SingletonComponent.class)
public abstract class RepositoryModule {
    
    @Binds
    public abstract UserRepository bindUserRepository(
        UserRepositoryImpl userRepositoryImpl);
}

@Module
@InstallIn(SingletonComponent.class)
public class NetworkModule {
    
    @Provides
    @Singleton
    public Retrofit provideRetrofit() {
        return new Retrofit.Builder()
            .baseUrl("https://api.example.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .build();
    }
}

改善点

  • 関心の分離: 各層の責任が明確
  • テスタビリティ: 層ごとの単体テスト
  • スケーラビリティ: 大規模開発への対応
  • 保守性: 機能追加・変更の影響範囲が限定的

問題点

  • 学習コスト: アーキテクチャの理解が必要
  • 初期コスト: 小規模アプリにはオーバーエンジニアリング
  • XML UI制約: 依然としてXMLベースのUI

主要技術

  • Hilt
  • UseCase pattern
  • Repository pattern
  • Network Bound Resource

第5期: Jetpack Compose現代(2020年〜現在)

Android開発史上最大のパラダイムシフトとも言える、宣言的UIフレームワークJetpack Composeの登場です。15年以上続いたXMLベースのUI定義から脱却し、Kotlinで完結する直感的なUI構築が可能になりました。StateFlowとComposeの組み合わせにより、状態管理とUI更新が完全に自動化され、開発者はUIの「あるべき姿」を定義するだけで、フレームワークが最適なレンダリングを行ってくれます。Clean ArchitectureとComposeの融合により、Android開発は過去最高の開発体験と保守性を実現しています。

時代背景

  • Google I/O 2019でJetpack Compose発表
  • 宣言的UIのパラダイムシフト
  • Flutter、React Nativeの影響
  • Kotlin Firstの推進

現代的なMVVM + Composeパターン

// UI State
data class UserListUiState(
    val isLoading: Boolean = false,
    val users: List<User> = emptyList(),
    val error: String? = null
)

// ViewModel with StateFlow
@HiltViewModel
class UserListViewModel @Inject constructor(
    private val getUserListUseCase: GetUserListUseCase
) : ViewModel() {
    
    private val _uiState = MutableStateFlow(UserListUiState())
    val uiState: StateFlow<UserListUiState> = _uiState.asStateFlow()
    
    init {
        loadUsers()
    }
    
    fun loadUsers() {
        viewModelScope.launch {
            _uiState.update { it.copy(isLoading = true) }
            
            getUserListUseCase()
                .catch { exception ->
                    _uiState.update { 
                        it.copy(
                            isLoading = false,
                            error = exception.message ?: "Unknown error"
                        )
                    }
                }
                .collect { users ->
                    _uiState.update { 
                        it.copy(
                            isLoading = false,
                            users = users,
                            error = null
                        )
                    }
                }
        }
    }
    
    fun retry() {
        loadUsers()
    }
}

// Jetpack Compose UI
@Composable
fun UserListScreen(
    viewModel: UserListViewModel = hiltViewModel()
) {
    val uiState by viewModel.uiState.collectAsState()
    
    when {
        uiState.isLoading -> {
            Box(
                modifier = Modifier.fillMaxSize(),
                contentAlignment = Alignment.Center
            ) {
                CircularProgressIndicator()
            }
        }
        uiState.error != null -> {
            ErrorScreen(
                message = uiState.error,
                onRetry = { viewModel.retry() }
            )
        }
        else -> {
            LazyColumn {
                items(uiState.users) { user ->
                    UserItem(user = user)
                }
            }
        }
    }
}

@Composable
private fun UserItem(user: User) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = 16.dp, vertical = 8.dp)
    ) {
        Column(
            modifier = Modifier.padding(16.dp)
        ) {
            Text(
                text = user.name,
                style = MaterialTheme.typography.headlineSmall
            )
            Text(
                text = user.email,
                style = MaterialTheme.typography.bodyMedium
            )
        }
    }
}

@Composable
private fun ErrorScreen(
    message: String,
    onRetry: () -> Unit
) {
    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text(
            text = message,
            style = MaterialTheme.typography.bodyLarge,
            textAlign = TextAlign.Center
        )
        Spacer(modifier = Modifier.height(16.dp))
        Button(onClick = onRetry) {
            Text("Retry")
        }
    }
}

Clean Architecture + Composeの実装

// UseCase (Domain Layer)
class GetUserListUseCase @Inject constructor(
    private val userRepository: UserRepository
) {
    suspend operator fun invoke(): Flow<List<User>> {
        return userRepository.getUserList()
    }
}

// Repository Implementation (Data Layer)
class UserRepositoryImpl @Inject constructor(
    private val remoteDataSource: UserRemoteDataSource,
    private val localDataSource: UserLocalDataSource
) : UserRepository {
    
    override fun getUserList(): Flow<List<User>> = flow {
        emit(localDataSource.getAllUsers())
        
        try {
            val remoteUsers = remoteDataSource.getUsers()
            localDataSource.insertUsers(remoteUsers)
            emit(localDataSource.getAllUsers())
        } catch (e: Exception) {
            // ローカルデータが存在する場合はそのまま使用
            val localUsers = localDataSource.getAllUsers()
            if (localUsers.isEmpty()) {
                throw e
            }
        }
    }
}

改善点

  • 宣言的UI: 状態に応じたUI自動更新
  • パフォーマンス: Composeの最適化されたレンダリング
  • 型安全性: Kotlinの利点を活かした開発
  • 簡潔性: XMLレイアウトが不要
  • テスタビリティ: Compose UIのテスト支援
  • ホットリロード: プレビュー機能による開発効率向上

新旧アーキテクチャ比較表

項目 Fat Activity MVP MVVM + AAC Clean + Compose
可読性 非常に悪い 普通 良い 非常に良い
テスタビリティ 不可能 普通 良い 非常に良い
保守性 非常に悪い 普通 良い 非常に良い
スケーラビリティ 不可能 悪い 普通 非常に良い
学習コスト 非常に低い 普通 高い 非常に高い
初期開発コスト 非常に低い 普通 高い 非常に高い
パフォーマンス 悪い 普通 良い 非常に良い

まとめ

Android開発のアーキテクチャは、プラットフォームの成熟とともに劇的に進化してきました。

各時代の特徴まとめ

  1. Fat Activity時代: シンプルだが保守困難
  2. MVP時代: テスタビリティの向上
  3. MVVM時代: ライフサイクル管理の標準化
  4. Clean Architecture時代: 大規模開発への対応
  5. Compose時代: 宣言的UIによるパラダイムシフト

重要なのは、プロジェクトの規模と要件に応じて適切なレベルのアーキテクチャを選択することです。すべてのプロジェクトに同じパターンを適用する必要はありません。

個人的には、現在のJetpack Compose + StateFlow + Clean Architectureの組み合わせは、過去のどの時代と比べても最も開発体験が良いと感じています。特に、宣言的UIによる直感的な開発と、型安全性による堅牢性の両立が素晴らしいですね。

皆さんも、過去の歴史を踏まえつつ、現在のベストプラクティスを活用して、より良いAndroidアプリを作っていきましょう!


参考資料

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?