20210221のAndroidに関する記事は8件です。

Hilt環境でのDefaultViewModelProviderFactoryの実装

3行まとめ

  • @DefaultActivityViewModelFactory および@DefaultFragmentViewModelFactory のQualifierを利用してマルチバインディングすると、ViewModelProvider.Factoryを指定しない形でViewModelを生成した際に使用される DefaultViewModelProviderFactoryの実装が可能である。
  • @HiltViewModel をViewModelに付与すると、ViewModelのパッケージ名をKeyとしたMap<String, ViewModel>型のオブジェクトグラフが登録される。
  • HiltViewModelFactoryでは、@HiltViewModelが付いたViewModelに対しては、内部で定義されたHiltViewModelFactoryが使用され、それ以外のViewModelに対しては、今回指定するViewModelProviderFactorySavedStateViewModelFactory()が使用される。

検証環境

com.google.dagger:hilt-android:2.32-alpha"

実装Sample

https://github.com/AkitoshiHashizume/HiltViewModelSample

はじめに

Hilt環境でのDefaultViewModelProviderFactory

Hiltでは、Activity/Fragmentに@AndroidEntryPointを追記すると、AppCompatActivity/Fragmentと該当のActivity/Fragmentクラスの間に、Hilt_〇〇Activity/Fragmentが自動生成される。

UserActivity.kt
@AndroidEntryPoint
class UserActivity : AppCompatActivity() {

  private val userViewModel: UserViewModel by viewModels()
  private val sampleViewModel: SampleViewModel by viewModels()

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    userViewModel.login()
    sampleViewModel.showId()
  }
}

// ↓↓↓ 自動生成された中間ファイル

public abstract class Hilt_UserActivity extends AppCompatActivity implements GeneratedComponentManagerHolder {
// 中略
}

そして、この中間ファイルはHasDefaultViewModelProviderFactory.javaで定義されているgetDefaultViewModelProviderFactory をOverrideしており、ViewModelProvider.Factoryを指定しない形でViewModelを生成した際に使用されるdefaultViewModelProviderFactoryを、Hiltライブラリ内で実装されているHiltViewModelFactory に差し替えている。

Hilt_UserActivity.kt
// ↓↓↓ 自動生成された中間ファイル
public abstract class Hilt_UserActivity extends AppCompatActivity implements GeneratedComponentManagerHolder {
  private volatile ActivityComponentManager componentManager;
// 中略
  @Override
  public ViewModelProvider.Factory getDefaultViewModelProviderFactory() {
    return DefaultViewModelFactories.getActivityFactory(this); // HiltViewModelFactoryの作成
  }
}

これにより、ViewModelに対して@HiltViewModelを付与するだけでViewModelに対するInjectをHilt経由で行えるのだが、この恩恵を受ける事ができる一方で、Hiltの中間ファイルがgetDefaultViewModelProviderFactory をOverrideしてしまっている為、@AndroidEntryPointを追記する以前には可能であったDefaultViewModelProviderFactoryのカスタマイズの両方を行う事ができなくなっている。
※もちろんActivity/Fragment側で、更にgetDefaultViewModelProviderFactory をOverrideする事でカスタマイズは可能だが、その場合、ViewModelへのHilt経由でのInjectを、Hiltライブラリ側に任せる事が出来なくなる。

UserViewModel.kt
@HiltViewModel
class UserViewModel @Inject constructor(
    private val userManager: UserManager
) : ViewModel() 

そこで本記事では、Hilt側で用意されている@DefaultActivityViewModelFactory および@DefaultFragmentViewModelFactory の両Qualifierを利用して、下記の両方を実現する方法を記載する。

  • @HiltViewModelのついたViewModelへのHilt経由でのInject
  • @AndroidEntryPointを追記する以前には可能であった(@HiltViewModelの付かないViewModelに対する)DefaultViewModelProviderFactoryのカスタマイズ

実現方法

時間の無い方の為に先に結論を記載しておくと、HiltのDefaultViewModelProviderFactoryでは、Daggerのマルチバインディング機能を使用している為、以下の様にViewModelProviderModuleを定義する事で、DefaultViewModelProviderFactoryの指定が可能となる。

ActivityへのDefaultViewModelProviderFactoryの実装

ViewModelProviderModule.kt
@Module
@InstallIn(ActivityComponent::class)
object ViewModelProviderModule {

    @Provides
    @IntoSet
    @DefaultActivityViewModelFactory
    @Suppress("UNCHECKED_CAST")
    fun provideDefaultActivityViewModelFactory(): ViewModelProvider.Factory {
        return object : ViewModelProvider.Factory {
            override fun <T : ViewModel?> create(modelClass: Class<T>): T {
                // ここでViewModel生成のカスタマイズを行う。
            }
        }
    }
}

FragmentへのDefaultViewModelProviderFactoryの実装

ViewModelProviderModule.kt
@Module
@InstallIn(ActivityComponent::class) // ActivityComponentに配置する点だけ注意
object ViewModelProviderModule {

    @Provides
    @IntoSet
    @DefaultFragmentViewModelFactory
    @Suppress("UNCHECKED_CAST")
    fun provideDefaultFragmentViewModelFactory(): ViewModelProvider.Factory {
        return object : ViewModelProvider.Factory {
            override fun <T : ViewModel?> create(modelClass: Class<T>): T {
                // ここでViewModel生成のカスタマイズを行う。
        }
    }

なおFragmentの方のコメントにも記載しているが、Hiltライブラリ内のDefaultViewModelFactories. ActivityModuleにおいては、ActivityComponentに対して、@DefaultActivityViewModelFactory@DefaultFragmentViewModelFactoryのマルチバインディングが登録されているので、FragmentへのDefaultViewModelProviderFactoryActivityComponentに配置する。

DefaultViewModelFactories.java
/** The activity module to declare the optional factories. */
  @Module
  @InstallIn(ActivityComponent.class)
  interface ActivityModule {
    @Multibinds
    @HiltViewModelMap.KeySet
    abstract Set<String> viewModelKeys();

    @Multibinds
    @DefaultActivityViewModelFactory
    Set<ViewModelProvider.Factory> defaultActivityViewModelFactory();

    @Multibinds
    @DefaultFragmentViewModelFactory
    Set<ViewModelProvider.Factory> defaultFragmentViewModelFactory();
  }

DefaultViewModelFactoriesの実装詳細

なぜ、上記ViewModelProviderModuleの定義だけで実現可能なのか?

ここからは、中間ファイルによって生成されるgetDefaultViewModelProviderFactory()の実装詳細にも触れつつ、Hiltライブラリ内のHiltViewModelFactoryの実装を紹介していく。
(少し長くなるがお付き合いいただきたい)

HiltViewModelFactoryの取得

以降では、@AndroidEntryPointのついたActivityの場合を例として、中間ファイルがOverrideした以下のメソッド内で使用されているDefaultViewModelFactories.javaの内容を追っていく。

Hilt_UserActivity.kt
  @Override
  public ViewModelProvider.Factory getDefaultViewModelProviderFactory() {
    return DefaultViewModelFactories.getActivityFactory(this);
  }

まずgetActivityFactory() の実装内容を覗いてみると、以下の様にActivityEntryPoint経由で、ActivityからInternalFactoryFactoryを取得し、InternalFactoryFactory#fromActivity()を実行する事でHiltViewModelFactory を生成している。
※このInternalFactoryFactoryの実装が、HiltViewModelFactory作成の要所となるので、詳細に触れていく。

DefaultViewModelFactories.java
public static ViewModelProvider.Factory getActivityFactory(ComponentActivity activity) {
    return EntryPoints.get(activity, ActivityEntryPoint.class)
        .getHiltInternalFactoryFactory() // InternalFactoryFactoryの取得
        .fromActivity(activity); //HiltViewModelFactory(=ViewModelProvider.Factory)の生成
  }
// (中略)

  @EntryPoint
  @InstallIn(ActivityComponent.class)
  public interface ActivityEntryPoint {
    InternalFactoryFactory getHiltInternalFactoryFactory();
  }
DefaultViewModelFactories.java
/** Internal factory for the Hilt ViewModel Factory. */
  public static final class InternalFactoryFactory {
// (中略)
    @Nullable private final ViewModelProvider.Factory defaultActivityFactory;
// (中略)
    ViewModelProvider.Factory fromActivity(ComponentActivity activity) {
      return getHiltViewModelFactory(activity,
          activity.getIntent() != null ? activity.getIntent().getExtras() : null,
          defaultActivityFactory);
    }
}

まず、上記コード内の以下の部分だが、

DefaultViewModelFactories.java
public static ViewModelProvider.Factory getActivityFactory(ComponentActivity activity) {
    return EntryPoints.get(activity, ActivityEntryPoint.class)
        .getHiltInternalFactoryFactory() // InternalFactoryFactoryの取得
// (中略)
}

EntryPoints.getの処理内容の実態は、第一引数に指定されたActivity/Fragmentなどから、自動生成された中間ファイルの所持するComponentManagerを取得し、そのComponentManager の保持するComponentを、第二引数で指定されたActivityEntryPointに型変換する事である。
つまり、自動生成されるActivityComponentからInternalFactoryFactoryを取得する処理が実行されている。

EntryPoints.java
/** Static utility methods for accessing objects through entry points. */
public final class EntryPoints {
  @Nonnull
  public static <T> T get(Object component, Class<T> entryPoint) {
    if (component instanceof GeneratedComponent) {
      return entryPoint.cast(component);
    } else if (component instanceof GeneratedComponentManager) {
      return entryPoint.cast(((GeneratedComponentManager<?>) component).generatedComponent());
    } else {
      throw new IllegalStateException();
    }
  }
}

EntryPoint経由でのInternalFactoryFactoryの取得を深堀してみる

流石に、ActivityComponentからInternalFactoryFactoryを取得する部分は、実例が無いと分かりにくいので、インターフェースであるActivityEntryPoint を具体化しているActivityCImpl(Hiltの自動生成ファイル)の中身を覗いてみる。

DaggerApp_HiltComponents_SingletonC.java
private final class ActivityCImpl extends App_HiltComponents.ActivityC {
      @Override
      public DefaultViewModelFactories.InternalFactoryFactory getHiltInternalFactoryFactory() {
        return DefaultViewModelFactories_InternalFactoryFactory_Factory.newInstance(
                ApplicationContextModule_ProvideApplicationFactory.provideApplication(DaggerApp_HiltComponents_SingletonC.this.applicationContextModule),
                keySetSetOfString(),
                new ViewModelCBuilder(),
                defaultActivityViewModelFactorySetOfViewModelProviderFactory(),
                defaultFragmentViewModelFactorySetOfViewModelProviderFactory()
        );
      }
}

ここで、DefaultViewModelFactories_InternalFactoryFactory_Factory.newInstanceという部分では、Daggerから特定のインスタンスを取得する際に使用される自動生成のメソッドを呼び出しており、その実態は、以下で定義されているDefaultViewModelFactories#InternalFactoryFactoryのコンストラクタを、Daggerに登録されているオブジェクトグラフの情報を元に呼び出している処理となる。
※Daggerの処理に関しては本記事のスコープ外な為、詳細は割愛するが、要はDaggerの登録情報を元にDefaultViewModelFactories#InternalFactoryFactoryをインスタンス化しているだけである。

DefaultViewModelFactories.java
/** Internal factory for the Hilt ViewModel Factory. */
  public static final class InternalFactoryFactory {

    private final Application application;
    private final Set<String> keySet;
    private final ViewModelComponentBuilder viewModelComponentBuilder;
    @Nullable private final ViewModelProvider.Factory defaultActivityFactory;
    @Nullable private final ViewModelProvider.Factory defaultFragmentFactory;

    @Inject
    InternalFactoryFactory(
            Application application,
        @HiltViewModelMap.KeySet Set<String> keySet,
        ViewModelComponentBuilder viewModelComponentBuilder,
        // These default factory bindings are temporary for the transition of deprecating
        // the Hilt ViewModel extension for the built-in support
        @DefaultActivityViewModelFactory Set<ViewModelProvider.Factory> defaultActivityFactorySet,
        @DefaultFragmentViewModelFactory Set<ViewModelProvider.Factory> defaultFragmentFactorySet) {
      this.application = application;
      this.keySet = keySet;
      this.viewModelComponentBuilder = viewModelComponentBuilder;
      this.defaultActivityFactory = getFactoryFromSet(defaultActivityFactorySet);
      this.defaultFragmentFactory = getFactoryFromSet(defaultFragmentFactorySet);
    }
}

そして、InternalFactoryFactory のコンストラクタInjectionの定義と、自動生成されたActivityCImplの両方をみてみると、InternalFactoryFactory のインスタンスは

引数 引数名:型 指定される情報
第1引数 application : Application Hiltが配布するApplicationContextをApplication型にキャストして指定
第2引数 @HiltViewModelMap.KeySet keySet : Set @HiltViewModelが付いたViewModelのパケージ名のSetが指定される(後述で補足)
第3引数 viewModelComponentBuilder : ViewModelComponentBuilder DefaultFragmentViewModelFactoryが要求される際に、new ViewModelCBuilder() で新規インスタンスが生成される
第4引数 @DefaultActivityViewModelFactory defaultActivityFactorySet : Set 実装方法で、マルチバインディングによって指定したViewModelProvider.FactoryのSet
第5引数 @DefaultFragmentViewModelFactory defaultFragmentFactorySet : Set (上記に同じく)

という情報を元にインスタンス化される。

(補足) @HiltViewModelを付与した際の自動生成ファイル

第3引数に関する補足だが、Hiltでは、下記の様にViewModelに @HiltViewModelを付与すると、

UserViewModel.kt
@HiltViewModel
class UserViewModel @Inject constructor(
    private val userManager: UserManager
) : ViewModel()

ViewModelのパッケージ情報から、@HiltViewModelMap.KeySet というQualifierでパケージ名の文字列がマルチバインディングされ、ViewModel本体も、パッケージ名をStringKeyとして@HiltViewModelMapというQualifierでMap<String, ViewModel> の形でオブジェクトグラフに登録される。

UserViewModel_HiltModules.kt
  @Module
  @InstallIn(ViewModelComponent.class)
  public abstract static class BindsModule {
    @Binds
    @IntoMap
    @StringKey("com.akitoshi.hashizume.hiltviewmodelsample.viewmodel.UserViewModel")
    @HiltViewModelMap
    public abstract ViewModel binds(UserViewModel vm);
  }

  @Module
  @InstallIn(ActivityRetainedComponent.class)
  public static final class KeyModule {
    private KeyModule() {
    }

    @Provides
    @IntoSet
    @HiltViewModelMap.KeySet
    public static String provide() {
      return "com.akitoshi.hashizume.hiltviewmodelsample.viewmodel.UserViewModel";
    }
  }

HiltViewModelFactoryのインスタンス作成

山場であるInternalFactoryFactoryの取得までを紹介できたので、最後に、HiltViewModelFactory作成まで一気に追っていく。

DefaultViewModelFactories.java
public static ViewModelProvider.Factory getActivityFactory(ComponentActivity activity) {
    return EntryPoints.get(activity, ActivityEntryPoint.class)
        .getHiltInternalFactoryFactory() // InternalFactoryFactoryの取得
        .fromActivity(activity); //ViewModelProvider.Factoryの生成
  }

DefaultViewModelFactories#getActivityFactory() では、InternalFactoryFactoryの取得をした後にInternalFactoryFactory#fromActivity() を実行しているが、InternalFactoryFactory#fromActivity() の実装は以下の様になっており、

DefaultViewModelFactories.java
/** Internal factory for the Hilt ViewModel Factory. */
  public static final class InternalFactoryFactory {

    private final Application application;  // 上の表で示したインスタンス作成時の第一引数
    private final Set<String> keySet;  // 第二引数
    private final ViewModelComponentBuilder viewModelComponentBuilder; // 第三引数
    @Nullable private final ViewModelProvider.Factory defaultActivityFactory; // 第四引数
    @Nullable private final ViewModelProvider.Factory defaultFragmentFactory; // 第五引数
// (中略)
    ViewModelProvider.Factory fromActivity(ComponentActivity activity) {
      return getHiltViewModelFactory(activity,
          activity.getIntent() != null ? activity.getIntent().getExtras() : null,
          defaultActivityFactory);
    }
// (中略)
    private ViewModelProvider.Factory getHiltViewModelFactory(
        SavedStateRegistryOwner owner,
        @Nullable Bundle defaultArgs,
        @Nullable ViewModelProvider.Factory extensionDelegate) {
      ViewModelProvider.Factory delegate = extensionDelegate == null
          ? new SavedStateViewModelFactory(application, owner, defaultArgs)
          : extensionDelegate;
      return new HiltViewModelFactory(
          owner, defaultArgs, keySet, delegate, viewModelComponentBuilder);
    }

最終的には、以下の情報を元に、HiltViewModelFactoryのインスタンスが作成される。

引数 引数名:型 指定される情報
第1引数 owner Activity/FragmentなどのSavedStateRegistryOwner
第2引数 defaultArgs Activity/Fragmentから取得したBundle
第3引数 keySet @HiltViewModelが付いたViewModelのパケージ名のSetが指定される
第4引数 delegate 実装方法で、マルチバインディングによって指定したViewModelProvider.FactoryのSetの「最初」の要素
第5引数 viewModelComponentBuilder ViewModelCBuilder() の新規インスタンス

HiltViewModelFactoryの実装内容

次に、HiltViewModelFactoryの実装内容を追っていく。
まず、HiltViewModelFactoryコンストラクタの実装は以下の様になっており、引数の情報から、hiltViewModelFactoryの作成が行われる。

HiltViewModelFactory.kt
public HiltViewModelFactory(
      @NonNull SavedStateRegistryOwner owner,  // 上の表で示したインスタンス作成時の第一引数
      @Nullable Bundle defaultArgs,  // 第二引数
      @NonNull Set<String> hiltViewModelKeys, // 第三引数
      @NonNull ViewModelProvider.Factory delegateFactory, // 第四引数
      @NonNull ViewModelComponentBuilder viewModelComponentBuilder // 第五引数 
) {
    this.hiltViewModelKeys = hiltViewModelKeys;
    this.delegateFactory = delegateFactory;
    this.hiltViewModelFactory =
        new AbstractSavedStateViewModelFactory(owner, defaultArgs) {
          @NonNull
          @Override
          @SuppressWarnings("unchecked")
          protected <T extends ViewModel> T create(
              @NonNull String key, @NonNull Class<T> modelClass, @NonNull SavedStateHandle handle) {
            ViewModelComponent component =
                viewModelComponentBuilder.savedStateHandle(handle).build();
            Provider<? extends ViewModel> provider =
                EntryPoints.get(component, ViewModelFactoriesEntryPoint.class)
                    .getHiltViewModelMap()
                    .get(modelClass.getName());
            if (provider == null) {
              throw new IllegalStateException(
                  "Expected the @HiltViewModel-annotated class '"
                      + modelClass.getName()
                      + "' to be available in the multi-binding of "
                      + "@HiltViewModelMap but none was found.");
            }
            return (T) provider.get();
          }
        };
  }

そして、肝心のHiltViewModelFactoryが実装しているViewModelProvider.Factoryがどうなっているかを確認すると、以下の様になっている。

HiltViewModelFactory.kt
@NonNull
  @Override
  public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
    // hiltViewModelKeysは、`@HiltViewModel`が付いたViewModelのパケージ名のSet
    if (hiltViewModelKeys.contains(modelClass.getName())) {
      return hiltViewModelFactory.create(modelClass);
    } else {
      //delegateFactoryは、今回マルチバインディングによって指定したViewModelProvider.Factory
      return delegateFactory.create(modelClass);
    }
  }

つまり、作成されようとしているViewModelのクラス名が、@HiltViewModelが付いたViewModelのパケージ名のSetに含まれる場合は、HiltによるInjectが要求されているとして、コンストラクタで作成したhiltViewModelFactory経由でViewModelの作成を行い、含まれなかった場合は、実装方法で、マルチバインディングによって指定したViewModelProvider.Factoryが使われる事となる。

※長くなったが、これで、下記のViewModelProviderModuleを定義する事で、DefaultViewModelProviderFactoryの実装の実装が可能な事が確認された。

ViewModelProviderModule.kt
@Module
@InstallIn(ActivityComponent::class)
object ViewModelProviderModule {

    @Provides
    @IntoSet
    @DefaultActivityViewModelFactory
    @Suppress("UNCHECKED_CAST")
    fun provideDefaultActivityViewModelFactory(): ViewModelProvider.Factory {
        return object : ViewModelProvider.Factory {
            override fun <T : ViewModel?> create(modelClass: Class<T>): T {
                // ここでViewModel生成のカスタマイズを行う。
            }
        }
    }
}

 注意

Hilt経由でInjectされるViewModelに対するDefaultViewModelProviderFactoryの実装

Hilt環境でのDefaultViewModelProviderFactoryの最後に示した様に、@DefaultActivityViewModelFactory / @DefaultFragmentViewModelFactoryは、@AndroidEntryPointをつける以前に実装可能であったHasDefaultViewModelProviderFactory. getDefaultViewModelProviderFactory() を置き換えるものであり、HiltViewModelFactoryによって生成されるViewModelの生成に対する共通処理を定義できるものでは無い。
その為、@HiltViewModelが付いたViewModelに対しては、ComponentActivity.viewModels()Fragment.viewModels() に対するExtensionを定義するなどして、共通処理を定義する事が必要となる。

@DefaultActivityViewModelFactoryを使用しなかった場合の@HiltViewModelの付かないViewModelに対するインスタンス化について

本筋とは外れるので、HiltViewModelFactoryのインスタンス作成では触れなかったが、InternalFactoryFactory.getHiltViewModelFactory()内の処理をみると、マルチバインディングによってViewModelProvider.Factoryを指定しない場合は、SavedStateViewModelFactoryのインスタンスが使用されている。
※つまり引数無しのコンストラクタによってインスタンス化される。

その為、引数を要求するViewModelなどをインスタンス化するときには、この記事で紹介した@DefaultActivityViewModelFactory@DefaultFragmentViewModelFactoryを指定する必要がある。

DefaultViewModelFactories.java
//extensionDelegateは、マルチバインディングによって指定したViewModelProvider.FactoryのSet要素 or 指定が無い場合はnull
//変数delegateは、HiltViewModelFactoryで使用される@HiltViewModelの付かないViewModelに対するViewModelFactory
ViewModelProvider.Factory delegate = extensionDelegate == null
          ? new SavedStateViewModelFactory(application, owner, defaultArgs)
          : extensionDelegate;

 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【過去の私】が作ったグラブルの救援検索アプリを【現在の私】がフルスクラッチしてやった

はじめまして、ユキギガンテと申します。
Androidエンジニアとしてお仕事をしています。
グラブルとアプリ開発が好きです。

このアプリが誕生した経緯

それは私がAndroidエンジニアになる前のこと。
エンジニアになるために独学していた中で、自分でも何かアプリを作ってみたいと思ったのがきっかけです。noteに当時の話を投稿してみたのでよろしければご覧ください。
私大文卒エンジニアの成り上がり#note

旧バージョン

 
こんな感じ。
未経験の時代に独学で作っただけあって、ソースコードは凄い。
本当に動くのか、実験用に色々試したコードの集合体だ。

新バージョン

 
こんな感じ。
2個以上の同時検索や、オートリトライ設定等、検索面での機能を追加してる。

技術紹介

今回このアプリに取り入れている技術要素を紹介していく。

CleanArchitecture × MultiModule

言わずと知れたクリーンアーキテクチャ。
私もかなり好んでおり、業務でもよく採用する。そこにマルチモジュールを採用することで依存関係を厳格化している。DIにはDaggerを使用。
実際のモジュール構成はこちら
JRWモジュール構成.png

RxJava/Kotlin

ストリームで複雑な処理を作るのが好きなので、Rxを好んで使用している。
Coroutineも良いがストリーム操作においてはRxのほうが強いかなぁ。
検索機能における設定項目を複数実装したがそれもすべて1つのストリームで実現している。

  //設定読み込み
    loadSetting.execute(NoRequestValue)
      .flatMap {
        enableDuplicateRetry = it.find{ setting -> setting.first == SettingModel.DUPLICATE_RETRY }?.second ?: false
        enableAutoRetry = it.find{ setting -> setting.first == SettingModel.AUTO_RETRY }?.second ?: false
    //検索実行
        searchRescue.execute(NoRequestValue)
      }
      .repeatWhen {
        it.takeWhile {
         (retryCount < RETRY_LIMIT_COUNT)
             && (enableAutoRetry || enableDuplicateRetry)
        }.delay(RETRY_INTERVAL_TIME_MILLIS, MILLISECONDS)
   ...以下省略

MotionLayout (ConstraintLayout2.0)

画面の動きはMotionLayoutを用いて実装した。
全てxmlで定義が可能で、この動きに関する実装でコードは1行も書いていない。
20210221_163627[1].gif

SharedElementTransition

画面遷移のアニメーション。昔GooglePlayで使われていたやつ。
最近はNavigationでの実装で遷移アニメーションの扱いが若干楽になった気がするが、遷移アニメーション自体は割と昔からある。
20210221_170229[1].gif

Service

この常駐windowはServiceを活用して実装している。
よくServiceにViewを持てるのかと聞かれるが、気合で持たせるのである。
20210221_174429[1].gif

おわりに

このアプリは私が初めて作ったAndroidのアプリです。
それを今までずっとアップデートしてきていて、ついにフルスクラッチしました。

昔の自分のコードを見るのも面白いものです。今だから気づけることがたくさんあるので自分の成長を実感できます。

でもやはり、実際にアプリを使ってもらって、
レビューを貰ったり、応援の言葉を貰ったり、時には厳しい意見を貰ったりしながら開発していけるのが個人開発の醍醐味ですかね!

よろしければ、是非インストールしてみてください!
https://play.google.com/store/apps/details?id=com.app.yuqinta.kinofjob&hl=ja

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Xamarin.Android での Switch の使い方

はじめに

こんにちは、私が説明するのはタイトルにあるようにXamarin.AndroidでのSwitchの使い方です。私が以前Switchを使おうとした時に、Xamarin.Formsの方の解説はたくさんあるのにXamarin.Androidの解説が全くと言っていいほど無かったので書いておきます。
プログラミング環境はVisual Studio 2019です。

作ったもの

今回は使い方を知りたかったのでSwitchを切り替えると文字が変わるだけのプログラムを書きました。
54671.jpg 54670.jpg
このようにSwitchが左の時はOff、右の時はOnと表示されるものです。

まずはコードの前にSwitchの説明をします。

Switchについて

SwitchはOn Offなど、2つの値を切り替える時に使うものです。Android標準のアラームにも使われていますね。SwitchをUIに使いたい場合、Buttonなどと同じようにxmlまたはxamlファイルに書きます。私はxmlファイルでの書き方しか把握していないためこの記事ではxamlファイルの場合は解説できません。

xmlのコード
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Switch
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/Switch" />
</LinearLayout>

Switchを置きたい場合上図のように書けば配置できます、私は親LayoutをLinearLayoutにしていますが、適切なものを選べば他のLayoutでもできるはずです。
Switchでも横幅や高さの指定は他のものと変わらずにできidは必須です。

続いてC#のコードです。
protected override void OnCreate(Bundle savedInstanceState)
    {
        base.OnCreate(savedInstanceState);
        Xamarin.Essentials.Platform.Init(this, savedInstanceState);
        // Set our view from the "main" layout resource
        SetContentView(Resource.Layout.activity_main);

        Switch sw = FindViewById<Switch>(Resource.Id.Switch);
        sw.Click += sw_Click;
    }

protected override void OnCreate(Bundle savedInstanceState) の中の下2行を書くだけでボタンをC#で使えるようにすることができます。このコードの場合、SwitchがClickされた時にsw_Click関数(後で定義する)が実行されるようになります。

次にコード上での扱い方を説明します。
まずSwitchクラスswが上図のコードでは宣言されていますが、その中に Checked というbool型の変数があり、Switchが左向き(初期の状態)ではfalse、右向き(初期の状態から1回押した状態)ではtrueが入っています。そのためsw内のCheckedさえ取り出すことができれば、あとは値を調べ、if文を使って値ごとの条件を書けば良いのです。取り出す方法は簡単で sw.Checked だけです。あとはこれがtureかfalseかを調べればいいだけですので簡単ですね。では始めに載せた画像のアプリのコードを見てみましょう。

アプリのコード

xml

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:textAppearance="?android:attr/textAppearanceLarge"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/text" />
    <Switch
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/Switch" />

</LinearLayout>

C#

MainActivity.cs
using Android.App;
using Android.OS;
using Android.Support.V7.App;
using Android.Runtime;
using Android.Widget;
using System;

namespace switch_exp
{
    [Activity(Label = "@string/app_name", Theme = "@style/AppTheme", MainLauncher = true)]
    public class MainActivity : AppCompatActivity
    {
        public TextView text { get; private set; }

        protected override void OnCreate(Bundle savedInstanceState)
        {
            base.OnCreate(savedInstanceState);
            Xamarin.Essentials.Platform.Init(this, savedInstanceState);
            SetContentView(Resource.Layout.activity_main);

            text = FindViewById<TextView>(Resource.Id.text);
            Switch sw = FindViewById<Switch>(Resource.Id.Switch);
            sw.Click += sw_Click;
        }

        private void sw_Click(object sender, EventArgs e)
        {
            Switch sw = FindViewById<Switch>(Resource.Id.Switch);
            if(sw.Checked == true)
            {
                text.Text = "On";
            }
            else
            {
                text.Text = "Off";
            }
        }

        public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)
        {
            Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);

            base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
        }
    }
}

protected override void OnCreate(Bundle savedInstanceState) はTextViewが増えただけで先程とほとんど変わりません。このコードで重要なのは private void sw_Click(object sender, EventArgs e) です。このアプリではSwitchの切り替えによって表示される文字が変化しますがそれをたったこれだけで書けてしまうのです。ifの条件式もsw.Checkedがtrueかfalseかを調べているだけで特に難しいわけでもなく、あとはifとelseそれぞれに処理を書いていけばSwitchは使えます。
ちなみにこのコードでは OnCreatesw_Click 両方でSwitch型のクラスswを宣言していますが、これを関数の外(public class MainActivity : AppCompatActivity直下)に書くのはダメでしたので、今回は両方の関数で宣言しました。そのためこの2つは名前は同じですが全く別のクラスです。しかしそれぞれのクラスには全く同じSwitchが紐付けられているので問題なく動作します。
もしかしたら何か方法があるのかもしれませんができちゃったので今回はやりません。

最後に

長々と書きましたがたったこれだけです、読むと難しく感じるかもしれませんがやってみると簡単だと感じるはずなのでぜひやってみてください。

それにしてもなんでXamarin.Androidの記事は全然無いんですかね、あったらこれを調べる必要無かったのに。。。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Flutterで文字列を入力できるダイアログを作ろう

Flutterでダイアログを表示するにはshowDialog()メソッドを用いる。
これを用いて「文字列を入力できるダイアログ」が作りたくなった。
ネットで調べまくったおかげでコードができたので、今回はそれを紹介する。

今回作るもの

aaa.jpg
真ん中のボタンをクリックしたら、
bbb.jpg
こう表示させる(なお、言語設定を省いたので中国語フォントになっています。)

コード

main.dart
import 'package:flutter/material.dart';

void main() => runApp(MaterialApp(
  home: TitleScreen(),
));

class TitleScreen extends StatefulWidget {
  @override
  _TitleScreenState createState() => _TitleScreenState();
}

class _TitleScreenState extends State<TitleScreen> {

  Future<void> InputDialog(BuildContext context) async {    //処理が重い(?)からか、非同期処理にする
    return showDialog(
        context: context,
        builder: (context) {
          return AlertDialog(
            title: Text('タイトル'),
            content: TextField(
              decoration: InputDecoration(hintText: "ここに入力"),
            ),
            actions: <Widget>[
              FlatButton(
                color: Colors.white,
                textColor: Colors.blue,
                child: Text('キャンセル'),
                onPressed: () {
                  Navigator.pop(context);
                },
              ),
              FlatButton(
                color: Colors.white,
                textColor: Colors.blue,
                child: Text('OK'),
                onPressed: () {
                  //OKを押したあとの処理
                },
              ),
            ],
          );
        });
  }

  @override
  Widget build(BuildContext context) => Center(
    child: RaisedButton(
      child: Text('ダイヤログを表示'),
      onPressed: () {
        InputDialog(context);
      },
    ),
  );
}

丸々コピペしたら使えます。

AlertDialogのcontentにTextFieldをそのまま指定しているだけなので、実は意外とシンプル。
TextFieldの中身を取得したい場合は、TextEditingControllerで指定してください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android round Icon の設定方法

Asset Studio を利用した icon と roundIcon の追加方法
1. Android Project の res フォルダで右クリック
2. New --> Image Asset をタップ
3. Android 8.0 に対応しているアプリであれば、Icon Type 似て Launcher Icons (Adaptive and Legacy)を選択
4. Name には指定したい名前を
5. Asset Type 似て、Image を選択。選択する Image の解像度によって、その後に生成される各解像度用のアイコンが設定されるみたいなので、綺麗なやつ選んでおいた方が良い・・?
6. 文字が入りきらない場合などは Scaling Reseize でサイズを変更する。
7. 設定が終了したら、Next をタップ
8. 最後に FINISH をタップして完了。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WebViewの高さがガタガタ変わらないようにしたい

WebViewの使い方として正しいかは分かりませんが、WebViewをRecyclerViewやScrollViewにandroid:layout_height="wrap_content"で配置して、Viewの高さをコンテンツの高さ分持たせるような使い方をしたくなることがあります。
こういったことをするとき、WebViewがコンテンツを読み込んでレンダリングが完了するまで高さが確定しなくて、他のViewの位置も連動して変わってしまう、ってのが悩みです。一方で、WebViewのようなものを配置すると言うことはアプリの更新を行わずコンテンツを柔軟に変更したい等の要求があるはずなので安易に最低限の高さみたいなものを設定する訳にも・・・という悩ましい問題があります。

まずは、最低限の高さを設定しつつ、それ以上になることを許容するという方法を考えてみましょう。
方法は他にもいろいろあるかとは思いますが、View自体の大きさはonMeasureの中でmeasuredWidth/measuredHeightが設定されることによって決まります。
ですので、基準値をstableHeightという変数に格納するとして、onMeasureをoverrideし、measuredHeightがstableHeight以下ならsetMeasuredDimensionで再設定、そうでなければstableHeightを更新する。
とすれば、最低限の高さを設定しつつ。それ以上になることを許容する動作をさせることができます。

private var stableHeight: Int = XXXX

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    val pendingHeight = measuredHeight
    if (pendingHeight < stableHeight) {
        setMeasuredDimension(measuredWidth, stableHeight)
    } else {
        stableHeight = pendingHeight
    }
}

次に、コンテンツを全部読み込んでみたら、stableHeightよりも小さかったという場合を考慮する必要がありますね。
この場合は、高さが確定した段階で、上記のpendingHeightを保持しておいて、その値でstableHeightをコールすれば良いですね。任意のWebページとなると様々な要因を考慮する必要がありますが、こういう埋め込み用の特定コンテンツならonPageFinishedのタイミングを拾えば十分でしょう。

高さがガタガタかわらないWebView

ということで、コードの全体像が以下になります。

まずは、レイアウトXMLからstableHeightを指定できるようにattrs.xmlでdeclare-styleableを定義しましょう。

attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="MyWebView">
        <attr name="stableHeight" format="dimension"/>
    </declare-styleable>
</resources>

WebViewを継承したクラスで以下のようにします。
setBackgroundColor(0)としているのは、そのままだと読み込みが行われるまで白い背景が描画されてしまうのでそれを防ぐためです。

class MyWebView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyle: Int = 0
) : WebView(context, attrs, defStyle) {
    private var stableHeight: Int = 0
    private var pendingHeight: Int = 0
    private var loading: Boolean = true
    private val updateStableHeight = Runnable { 
        updateStableHeight()
    }

    init {
        context.obtainStyledAttributes(attrs, R.styleable.MyWebView).use {
            stableHeight = it.getDimensionPixelSize(R.styleable.MyWebView_stableHeight, 0)
        }
        setBackgroundColor(Color.TRANSPARENT)
    }

    fun updateStableHeight() {
        if (pendingHeight == stableHeight) return
        stableHeight = pendingHeight
        requestLayout()
    }

    fun onPageStarted() {
        loading = true
        removeCallbacks(updateStableHeight)
    }

    fun onPageFinished() {
        loading = false
        postUpdateStableHeight()
    }

    private fun postUpdateStableHeight() {
        removeCallbacks(updateStableHeight)
        if (loading || pendingHeight == stableHeight) return
        postDelayed(updateStableHeight, 500)
    }

    private fun updateStableHeight() {
        if (pendingHeight == stableHeight) return
        stableHeight = pendingHeight
        requestLayout()
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        pendingHeight = measuredHeight
        if (pendingHeight < stableHeight) {
            setMeasuredDimension(measuredWidth, stableHeight)
        } else {
            stableHeight = pendingHeight
        }
        postUpdateStableHeight()
    }
}

onPageStartedとonPageFinishedはWebViewClientからコールする必要がありますね。また、JSなどで動的に高さが変わる場合で他に適切なトリガがあるならそのタイミングで更新すれば良いかなと思います。

webView.webViewClient = object : WebViewClient() {
    override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) {
        (view as? MyWebView)?.onPageStarted()
    }
    override fun onPageFinished(view: WebView, url: String?) {
        (view as? MyWebView)?.onPageFinished()
    }
}

汎用的なViewとして作るのはちょっと難しいですが、特定目的のカスタムViewとしてなら、この方法で十分ではないかと思います。stableHeightは永続化データとして持っておくというのもありかもしれません。

以上です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AndroidのEditTextで入力された文字がメールアドレスかどうか判定する(Kotlin)

はじめに

初投稿です。Android開発学習中の大学生です。学んだことをまとめて頭の中を整理するために書きます。なので僕と同じ初心者向けの内容です。

背景

Android開発で会員登録の機能を作っているときに、入力された文字列がメールアドレスかどうかの判定方法がわからず困りました。いろいろ調べたり知人のエンジニアの方に教わったことをまとめます。

解決方法

大きく分けて二つの方法があります。
1.正規表現でメールアドレスのパターンを作って、それと入力された文字列が合致するか調べる方法。
2.Androidにデフォルトで存在するPatternsクラスのメソッドを用いて調べる方法。

あくまで僕の解釈ですが、1の方が本質的なやり方で2は飛び道具みたいな感じかなと思いました。ただ1の方が正規表現のことを理解していないとできないのでめんどくさめ。あと抜け漏れが発生しやすいかも。まあでも2のやり方も大元では1と同じことやってそう。

1の方法について

まず、正規表現の概念と定型パターンは
概念 : https://userweb.mnet.ne.jp/nakama/
定型パターン : https://qiita.com/grrrr/items/0b35b5c1c98eebfa5128
がわかりやすかったです。

入力された文字列がメールアドレスならばtrueそれ以外はfalseを返すサンプルメソッド。

 fun isEmailAddress(): Boolean {
        val mail = editText.text.toString()
        return mail.matches(Regex("[a-zA-Z0-9._-]+@[a-z]{2,}+\\.+[a-z]{2,}"))
    }

※正規表現の部分は抜け漏れがあるかもしれないので参考程度に思っといてください。

純粋に入力された文字列が正規表現で設定した型とあってるかを見る方法です。

2の方法について

なんも考えず使える、ただただ便利な方法です。抜け漏れも起こりにくいと思います。起きたらAndroidのせいです。
公式のドキュメント : https://developer.android.com/reference/android/util/Patterns

fun isEmailAddress(): Boolean {
        val mail = editText.text.toString()
        return Patterns.EMAIL_ADDRESS.matcher(mail).matches()
    }

脳死で使いましょう。

結論

多分1の方法を理解したうえで、2の方法を使うっていうのがベストです。

追記

意外と記事書くのが楽しかった。あと内容が間違っている可能性があるので気を付けてください。あとこれはメールアドレスだけでなく、電話番号とかほかにも応用できます。

4:00に深夜テンションで書いたのでもう寝ます。

※コメントで、誤解を招く表記をご指摘頂いたので更新しました。コメント頂いた@sdkeiさんありがとうございます。
2/21 22:00に更新

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

外部通信は非同期で行いましょう

Firebaseなどの外部APIにアクセスするときは必ず非同期処理でかく必要があります。
以前はMVVMでその方法を説明しましたが、Viewに情報を反映しなくていい場合などの状況下ではViewModelは必要ないので、Activity/Fragmentから直接Repositoryを呼び出すことになります。ほぼMVVMとやり方は変わりませんが忘れっぽい私のためにここに残しておきます。
ちなみに、サーバーサイドの処理は非同期にする必要はありません。

Activity.kt
private val userInfoRepository = UserInfoRepository()
private val notificationRepository = NotificationRepository()

GlobalScope.launch {
      notificationRepository.save(uid, token)
}

GlobalScope.launch {
       // 引数のuidと一致するuidのデータを取得
       result = userInfoRepository.getUser(uid)
       Log.d("TAG", result.toString())
}                    

こんなかんじでGlobalScope{}を使って非同期処理を実行できます。

RepositoryはMVVMの記事と全く変わりません。
例えばこんなかんじです。

NotificationRepository.kt
class NotificationRepository {
    // uidとトークンを登録
    suspend fun save(uid: String?, token: String?): Task<Void> {
        return suspendCoroutine { cont ->
            val db = FirebaseFirestore.getInstance()
            val testNotification = TestNotificationDataClass(uid, token)
            val task = db.collection("testNotification")
                .document()
                .set(testNotification)
            Log.d("TOKEN", "about to enter addOnCompleteListener")
            task.addOnCompleteListener {
                Log.d("TOKEN", "in the addOnCompleteListener")
                cont.resume(it)
            }
            Log.d("TOKEN", "finish addOnCompleteListener")
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む