Androidアプリケーションの新しいアーキテクチャ-実際に試してみてください

みなさんこんにちは。 前回のGoogle I / Oで、ついにAndroidアプリケーションのアーキテクチャに関するGoogleの公式ビジョンと、その実装のためのライブラリを発表しました。 10年も経っていません。 もちろん、私はすぐにそこで提供されたものを試してみたかったです。







注意:ライブラリはアルファ版であるため、互換性に影響する変更が予想されます。







ライフサイクル


新しいアーキテクチャの主なアイデアは、アクティビティとフラグメントからロジックを最大限に削除することです。 会社は、これらのコンポーネントをシステムに属し、開発者の責任範囲に属していないと見なすべきだと主張しています。 アイデア自体は新しいものではなく、MVP / MVVPは現在アクティブに使用されています。 ただし、コンポーネントのライフサイクルとの関係は常に開発者の良心にとどまっています。







これはそうではありません。 クラスLifecycleLifecycleActivityLifecycleFragmentを含む新しいパッケージandroid.arch.lifecycleが提示されます。 近い将来、 LifecycleOwnerインターフェースの実装を通じて、特定のライフサイクルに存在するシステムのすべてのコンポーネントがライフサイクルを提供すると想定されています。







public interface LifecycleOwner { Lifecycle getLifecycle(); }
      
      





パッケージはまだアルファ版であり、APIを安定版と混合できないため、LifecycleActivityおよびLifecycleFragmentクラスが追加されました。 パッケージが安定した状態になると、LifecycleOwnerはFragmentおよびAppCompatActivityに実装され、LifecycleActivityおよびLifecycleFragmentは削除されます。







ライフサイクルには、コンポーネントのライフサイクルの現在の状態が含まれており、LifecycleObserverがライフサイクル遷移イベントサブスクライブできるようにします。 良い例:







 class MyLocationListener implements LifecycleObserver {  private boolean enabled = false; private final Lifecycle lifecycle;  public MyLocationListener(Context context, Lifecycle lifecycle, Callback callback) { this.lifecycle = lifecycle; this.lifecycle.addObserver(this); // -   }  @OnLifecycleEvent(Lifecycle.Event.ON_START)  void start() {    if (enabled) {      //        }  }  public void enable() {    enabled = true;    if (lifecycle.getState().isAtLeast(STARTED)) {      //    , //        }  }  @OnLifecycleEvent(Lifecycle.Event.ON_STOP)  void stop() {    //      } }
      
      





あとは、MyLocationListenerを作成して、それを忘れるだけです。







 class MyActivity extends LifecycleActivity { private MyLocationListener locationListener; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); locationListener = new MyLocationListener(this, this.getLifecycle(), location -> { //  , ,    }); // -     Util.checkUserStatus(result -> { if (result) { locationListener.enable(); } }); } }
      
      





ライブデータ


LiveDatarxJavaのObservableに類似していますが、ライフサイクルの存在を認識しています。 LiveDataには値が含まれており、その各変更はオブザーバーに送られます。







LiveDataの3つの主要なメソッド:







setValue()-値を変更し、オブザーバーに通知します;

onActive()-少なくとも1人のアクティブなオブザーバーが現れました。

onInactive()-アクティブなオブザーバはこれ以上ありません。







したがって、LiveDataにアクティブなオブザーバーがない場合、データの更新を停止できます。







アクティブなオブザーバーとは、ライフサイクルがSTARTEDまたはRESUMED状態にあるオブザーバーです。 新しいアクティブなオブザーバーがLiveDataに参加すると、すぐに現在の値が取得されます。







これにより、LiveDataインスタンスを静的変数に保存し、UIコンポーネントからサブスクライブできます。







 public class LocationLiveData extends LiveData<Location> {  private LocationManager locationManager;  private SimpleLocationListener listener = new SimpleLocationListener() {    @Override    public void onLocationChanged(Location location) {      setValue(location);    }  };  public LocationLiveData(Context context) {    locationManager = (LocationManager) context.getSystemService(        Context.LOCATION_SERVICE);  }  @Override  protected void onActive() {    locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, listener);  }  @Override  protected void onInactive() {    locationManager.removeUpdates(listener);  } }
      
      





通常の静的変数を作成しましょう:







 public final class App extends Application { private static LiveData<Location> locationLiveData = new LocationLiveData(); public static LiveData<Location> getLocationLiveData() { return locationLiveData; } }
      
      





また、たとえば、2つのアクティビティで場所の変更をサブスクライブします。







 public class Activity1 extends LifecycleActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity1); getApplication().getLocationLiveData().observe(this, (location) -> { // do something }) } } public class Activity2 extends LifecycleActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity2); getApplication().getLocationLiveData().observe(this, (location) -> { // do something }) } }
      
      





observeメソッドはLifecycleOwnerを最初のパラメーターとして使用するため、各サブスクリプションを特定のアクティビティのライフサイクルにリンクすることに注意してください。







アクティビティのライフサイクルがDESTROYEDサブスクリプションに入るとすぐに破棄されます。







このアプローチの利点は、コードからスパゲッティが発生することはなく、メモリリークが発生しないことと、強制終了されたアクティビティでハンドラーが呼び出されないことです。







ViewModel


ViewModelは、UIのデータウェアハウスであり、たとえば構成の変更などのUIコンポーネントの破壊に耐えることができます(はい、MVVMは現在、公式に推奨されるパラダイムです)。 新しく作成されたアクティビティは、以前に作成されたモデルに再接続します。







 public class MyActivityViewModel extends ViewModel { private final MutableLiveData<String> valueLiveData = new MutableLiveData<>(); public LiveData<String> getValueLiveData() { return valueLiveData; } } public class MyActivity extends LifecycleActivity { MyActivityViewModel viewModel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity); viewModel = ViewModelProviders.of(this).get(MyActivityViewModel.class); viewModel.getValueLiveData().observe(this, (value) -> { //     }); } }
      
      





ofメソッドパラメーターは、モデルインスタンスのスコープを定義します。 つまり、同じ値がofに渡されると、クラスの同じインスタンスが返されます。 インスタンスがまだない場合は作成されます。







スコープとしては、自分自身へのリンクだけでなく、もっとsomethingなものを渡すことができます。 現在、3つのアプローチが推奨されています。







  1. アクティビティ自体が転送されます。
  2. フラグメントはそれ自体を伝えます。
  3. フラグメントはそのアクティビティを転送します。


3番目の方法では、共通のViewModelを介してフラグメントとそのアクティビティ間のデータ転送を整理できます。 つまり、フラグメントの引数やアクティビティ用の特別なインターフェイスを作成する必要がなくなりました。 誰もお互いについて何も知らない。







モデルインスタンスがバインドされているすべてのコンポーネントを破棄すると、そのインスタンスに対してonClearedイベントが発生し、モデルが破棄されます。







重要な点:ViewModelは通常、同じモデルインスタンスを使用するコンポーネントの数を知らないため、モデル内のコンポーネントへの参照を保存することはありません。







ルーム永続ライブラリ


私たちの幸福は、アプリケーションの早すぎる終了後にデータをローカルに保存する能力がなければ不完全です。 ここでは、SQLiteがアクセス可能な「箱から出して」すぐに助けを求めています。 ただし、主にコンパイル時にコードをチェックする方法を提供しないため、データベースAPIはかなり不便です。 アプリケーションの実行時にSQL式のタイプミスについて既に学習しており、クライアントではないとしてもいいです。







しかし、これは過去のものです。Googleは、コンパイル中にSQL式の静的分析を行うORMライブラリを導入しました。







EntityDAO 、およびDatabaseの少なくとも3つのコンポーネントを実装する必要があります







エンティティは、テーブル内の1つのエントリです。







 @Entity(tableName = «users») public class User() { @PrimaryKey public int userId; public String userName; }
      
      





DAO(データアクセスオブジェクト)-特定のタイプのレコードの処理をカプセル化するクラス:







 @Dao public interface UserDAO { @Insert(onConflict = REPLACE) public void insertUser(User user); @Insert(onConflict = REPLACE) public void insertUsers(User… users); @Delete public void deleteUsers(User… users); @Query(«SELECT * FROM users») public LiveData<List<User>> getAllUsers(); @Query(«SELECT * FROM users WHERE userId = :userId LIMIT 1») LiveData<User> load(int userId); @Query(«SELECT userName FROM users WHERE userId = :userId LIMIT 1») LiveData<String> loadUserName(int userId); }
      
      





DAOはインターフェイスであり、クラスではないことに注意してください。 その実装はコンパイル中に生成されます。







私の意見では、最も驚くべきことは、存在しないテーブルとフィールドを参照する式がQueryに渡されるとコンパイルがクラッシュすることです。







クエリには、テーブル結合を式として含めることもできます。 ただし、エンティティ自体には他のテーブルへのリンクを含めることはできません。これは、それらにアクセスする際の遅延データの読み込みが同じストリームで開始され、おそらくUIストリームになるためです。 したがって、Googleはそのような慣行を完全に禁止することを決定しました。







また、コードがテーブルのエントリを変更するとすぐに、このテーブルを含むすべてのLiveDataが更新されたデータをオブザーバーに転送することも重要です。 つまり、アプリケーションのデータベースは「究極の真実」になりました。 このアプローチにより、アプリケーションのさまざまな部分のデータの不整合を最終的に取り除くことができます。







それだけでなく、Googleは将来、変更を1行ごとに追跡し、現在のように表形式では追跡しないことを約束します。







最後に、データベース自体を指定する必要があります。







 @Database(entities = {User.class}, version = 1) public abstract class AppDatabase extends RoomDatabase { public abstract UserDAO userDao(); }
      
      





ここではコード生成も使用されるため、クラスではなくインターフェイスを記述しています。







ApplicationクラスまたはDaggerモジュールでシングルトンデータベースを作成します。







 AppDatabase database = Room.databaseBuilder(context, AppDatabase.class, "data").build();
      
      





私たちはそこからDAOを取得し、あなたは働くことができます:







 database.userDao().insertUser(new User(…));
      
      





DAOメソッドの最初の呼び出しで、指定されている場合、テーブルが自動的に作成/再作成されるか、スキーマを更新するためのSQLスクリプトが実行されます。 スキーマ更新スクリプトは、移行オブジェクトを介して指定されます。







 AppDatabase database = Room.databaseBuilder(context, AppDatabase.class, "data") .addMigration(MIGRATION_1_2) .addMigration(MIGRATION_2_3) .build(); static Migration MIGRATION_1_2 = new Migration(1, 2) { @Override public void migrate(SupportSQLDatabase database) { database.execSQL(…); } } static Migration MIGRATION_2_3 = new Migration(2, 3) { … }
      
      





さらに、AppDatabaseで注釈のスキームの現在のバージョンを示すことを忘れないでください。







もちろん、スキーマを更新するためのSQLスクリプトは単なる文字列である必要があり、しばらくするとテーブルクラスが大幅に変更されるため、外部定数に依存しないでください。古いバージョンのデータベースの更新はエラーなしで実行する必要があります。







すべてのスクリプトの実行が終了すると、ベースクラスとエンティティクラスのコンプライアンスの自動チェックが実行され、不一致がある場合は例外がスローされます。







注意:実際のバージョンから最新バージョンへの移行のチェーンを作成できなかった場合、データベースは削除され、再作成されます。







私の意見では、スキーム更新アルゴリズムには欠点があります。 お使いのデバイスに古いデータベースがある場合、データベースは更新されますが、すべて問題ありません。 ただし、データベースがなく、必要なバージョンが1以上で、移行のセットが指定されている場合、データベースはエンティティに基づいて作成され、移行は実行されません。

移行では、テーブルの構造を変更することしかできませんが、テーブルをデータで埋めることはできません。 これは残念です。 このアルゴリズムの改善が期待できると思います。







きれいな建築


上記のすべてのエンティティは、提案されている新しいアプリケーションアーキテクチャのブリックです。 Googleはどこにもきれいなアーキテクチャを書いていないことに注意してください、これは私の側からのいくつかの自由ですが、考え方は似ています。

画像

エンティティはそれより上のエンティティについて何も知りません。







モデルとリモートデータソースは、それぞれデータをローカルに保存し、ネットワーク経由でクエリを実行します。 リポジトリは、キャッシュを管理し、ビジネス目標に従って個々のエンティティを統合します。 リポジトリクラスは、開発者にとってはある種の抽象化であり、特別なリポジトリベースクラスは存在しません。 最後に、ViewModelは特定のUIに適した形式でさまざまなリポジトリを統合します。







レイヤー間のデータは、サブスクリプションを通じてLiveDataに送信されます。









小さなデモアプリケーションを作成しました。 多くの都市の現在の天気を表示します。 簡単にするために、都市のリストは事前定義されています。 データプロバイダーとして、 OpenWeatherMapサービスが使用されます。







2つのフラグメントがあります。都市のリスト(CityListFragment)と選択した都市の天気(CityFragment)です。 両方のフラグメントはMainActivityにあります。







アクティビティとフラグメントは同じMainActivityViewModelを使用します。







MainActivityViewModelはWeatherRepositoryにデータを要求しています。







WeatherRepositoryはデータベースから古いデータを返し、すぐにネットワーク経由で更新されたデータの要求を開始します。 更新されたデータが正常に到着すると、データベースに保存され、ユーザーが画面上で更新します。







正常に動作させるには、WeatherRepositoryにAPIキーを登録する必要があります。 キーは、OpenWeatherMapへの登録後に無料で取得できます。







GitHubリポジトリ







革新は非常に興味深いように見えますが、それを維持する価値がある間、すべてをやり直そうという衝動です。 これはアルファのみであることを忘れないでください。







コメントや提案を歓迎します。 やった!








All Articles