Muitos apps usam o Hilt para injetar comportamentos diferentes em variantes de build distintas. Isso pode ser particularmente útil ao fazer uma microcomparação do app, porque permite trocar um componente que pode distorcer os resultados. Por exemplo, o snippet de código a seguir mostra um repositório que busca e classifica uma lista de nomes:
Kotlin
class PeopleRepository @Inject constructor( @Kotlin private val dataSource: NetworkDataSource, @Dispatcher(DispatcherEnum.IO) private val dispatcher: CoroutineDispatcher ) { private val _peopleLiveData = MutableLiveData<List<Person>>() val peopleLiveData: LiveData<List<Person>> get() = _peopleLiveData suspend fun update() { withContext(dispatcher) { _peopleLiveData.postValue( dataSource.getPeople() .sortedWith(compareBy({ it.lastName }, { it.firstName })) ) } } }}
Java
public class PeopleRepository { private final MutableLiveData<List<Person>> peopleLiveData = new MutableLiveData<>(); private final NetworkDataSource dataSource; public LiveData<List<Person>> getPeopleLiveData() { return peopleLiveData; } @Inject public PeopleRepository(NetworkDataSource dataSource) { this.dataSource = dataSource; } private final Comparator<Person> comparator = Comparator.comparing(Person::getLastName) .thenComparing(Person::getFirstName); public void update() { Runnable task = new Runnable() { @Override public void run() { peopleLiveData.postValue( dataSource.getPeople() .stream() .sorted(comparator) .collect(Collectors.toList()) ); } }; new Thread(task).start(); } }
Se você incluir uma chamada de rede ao fazer um comparativo de mercado, implemente uma chamada de rede falsa para conseguir um resultado mais preciso.
Incluir uma chamada de rede real ao fazer comparativos de mercado dificulta a interpretação dos resultados. As chamadas de rede podem ser afetadas por muitos fatores externos, e a duração delas pode variar entre as iterações de execução da comparação. A duração das chamadas de rede pode ser maior do que a classificação.
Implementar uma chamada de rede simulada usando o Hilt
A chamada para dataSource.getPeople(), como mostrado no exemplo anterior,
contém uma chamada de rede. No entanto, a instância NetworkDataSource é injetada
pelo Hilt, e você pode substituí-la pela seguinte implementação falsa para
comparação:
Kotlin
class FakeNetworkDataSource @Inject constructor( private val people: List<Person> ) : NetworkDataSource { override fun getPeople(): List<Person> = people }
Java
public class FakeNetworkDataSource implements NetworkDataSource{ private List<Person> people; @Inject public FakeNetworkDataSource(List<Person> people) { this.people = people; } @Override public List<Person> getPeople() { return people; } }
Essa chamada de rede falsa foi projetada para ser executada o mais rápido possível quando você chama
o método getPeople(). Para que o Hilt possa injetar isso, o seguinte
provedor é usado:
Kotlin
@Module @InstallIn(SingletonComponent::class) object FakekNetworkModule { @Provides @Kotlin fun provideNetworkDataSource(@ApplicationContext context: Context): NetworkDataSource { val data = context.assets.open("fakedata.json").use { inputStream -> val bytes = ByteArray(inputStream.available()) inputStream.read(bytes) val gson = Gson() val type: Type = object : TypeToken<List<Person>>() {}.type gson.fromJson<List<Person>>(String(bytes), type) } return FakeNetworkDataSource(data) } }
Java
@Module @InstallIn(SingletonComponent.class) public class FakeNetworkModule { @Provides @Java NetworkDataSource provideNetworkDataSource( @ApplicationContext Context context ) { List<Person> data = new ArrayList<>(); try (InputStream inputStream = context.getAssets().open("fakedata.json")) { int size = inputStream.available(); byte[] bytes = new byte[size]; if (inputStream.read(bytes) == size) { Gson gson = new Gson(); Type type = new TypeToken<ArrayList<Person>>() { }.getType(); data = gson.fromJson(new String(bytes), type); } } catch (IOException e) { // Do something } return new FakeNetworkDataSource(data); } }
Os dados são carregados de recursos usando uma chamada de E/S de tamanho potencialmente variável.
No entanto, isso é feito durante a inicialização e não causa irregularidades
quando getPeople() é chamado durante o comparativo de mercado.
Alguns apps já usam simulações em builds de depuração para remover dependências de back-end. No entanto, é preciso fazer a comparação em um build o mais próximo possível do build de lançamento. O restante deste documento usa uma estrutura de vários módulos e variantes, conforme descrito em Configuração completa do projeto.
Há três módulos:
benchmarkable: contém o código para comparar.benchmark: contém o código de comparativo de mercado.app: contém o restante do código do app.
Cada um dos módulos anteriores tem uma variante de build chamada benchmark, além das
variantes comuns debug e release.
Configurar o módulo de comparativo de mercado
O código da chamada de rede simulada está no conjunto de origem debug do módulo
benchmarkable, e a implementação de rede completa está no conjunto de origem release
do mesmo módulo. O arquivo de recursos que contém os dados retornados pela
implementação simulada está no conjunto de origem debug para evitar qualquer aumento de APK no
build release. A variante benchmark precisa ser baseada em release e
usar o conjunto de origem debug. A configuração do build para a variante benchmark
do módulo benchmarkable que contém a implementação simulada é a seguinte:
Kotlin
android { ... buildTypes { release { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } create("benchmark") { initWith(getByName("release")) } } ... sourceSets { getByName("benchmark") { java.setSrcDirs(listOf("src/debug/java")) assets.setSrcDirs(listOf("src/debug/assets")) } } }
Groovy
android { ... buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' ) } benchmark { initWith release } } ... sourceSets { benchmark { java.setSrcDirs ['src/debug/java'] assets.setSrcDirs(listOf ['src/debug/assets'] } } }
No módulo benchmark, adicione um executor de teste personalizado que crie um Application
para que os testes sejam executados com suporte ao Hilt, da seguinte maneira:
Kotlin
class HiltBenchmarkRunner : AndroidBenchmarkRunner() { override fun newApplication( cl: ClassLoader?, className: String?, context: Context? ): Application { return super.newApplication(cl, HiltTestApplication::class.java.name, context) } }
Java
public class JavaHiltBenchmarkRunner extends AndroidBenchmarkRunner { @Override public Application newApplication( ClassLoader cl, String className, Context context ) throws InstantiationException, IllegalAccessException, ClassNotFoundException { return super.newApplication(cl, HiltTestApplication.class.getName(), context); } }
Isso faz com que o objeto Application em que os testes são executados estenda a classe
HiltTestApplication. Faça as seguintes mudanças na configuração
do build:
Kotlin
plugins { alias(libs.plugins.android.library) alias(libs.plugins.benchmark) alias(libs.plugins.jetbrains.kotlin.android) alias(libs.plugins.kapt) alias(libs.plugins.hilt) } android { namespace = "com.example.hiltmicrobenchmark.benchmark" compileSdk = 34 defaultConfig { minSdk = 24 testInstrumentationRunner = "com.example.hiltbenchmark.HiltBenchmarkRunner" } testBuildType = "benchmark" buildTypes { debug { // Since isDebuggable can't be modified by Gradle for library modules, // it must be done in a manifest. See src/androidTest/AndroidManifest.xml. isMinifyEnabled = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "benchmark-proguard-rules.pro" ) } create("benchmark") { initWith(getByName("debug")) } } } dependencies { androidTestImplementation(libs.bundles.hilt) androidTestImplementation(project(":benchmarkable")) implementation(libs.androidx.runner) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.junit) implementation(libs.androidx.benchmark) implementation(libs.google.dagger.hiltTesting) kaptAndroidTest(libs.google.dagger.hiltCompiler) androidTestAnnotationProcessor(libs.google.dagger.hiltCompiler) }
Groovy
plugins { alias libs.plugins.android.library alias libs.plugins.benchmark alias libs.plugins.jetbrains.kotlin.android alias libs.plugins.kapt alias libs.plugins.hilt } android { namespace = 'com.example.hiltmicrobenchmark.benchmark' compileSdk = 34 defaultConfig { minSdk = 24 testInstrumentationRunner 'com.example.hiltbenchmark.HiltBenchmarkRunner' } testBuildType "benchmark" buildTypes { debug { // Since isDebuggable can't be modified by Gradle for library modules, // it must be done in a manifest. See src/androidTest/AndroidManifest.xml. minifyEnabled true proguardFiles( getDefaultProguardFile('proguard-android-optimize.txt'), 'benchmark-proguard-rules.pro' ) } benchmark { initWith debug" } } } dependencies { androidTestImplementation libs.bundles.hilt androidTestImplementation project(':benchmarkable') implementation libs.androidx.runner androidTestImplementation libs.androidx.junit androidTestImplementation libs.junit implementation libs.androidx.benchmark implementation libs.google.dagger.hiltTesting kaptAndroidTest libs.google.dagger.hiltCompiler androidTestAnnotationProcessor libs.google.dagger.hiltCompiler }
O exemplo anterior faz o seguinte:
- Aplica os plug-ins necessários do Gradle ao build.
- Especifica que o executor de testes personalizado é usado para executar os testes.
- Especifica que a variante
benchmarké o tipo de teste para este módulo. - Adiciona a variante
benchmark. - Adiciona as dependências necessárias.
É necessário mudar o testBuildType para garantir que o Gradle crie a
tarefa connectedBenchmarkAndroidTest, que realiza o comparativo de mercado.
Criar o microbenchmark
O comparativo de mercado é implementado da seguinte maneira:
Kotlin
@RunWith(AndroidJUnit4::class) @HiltAndroidTest class PeopleRepositoryBenchmark { @get:Rule val benchmarkRule = BenchmarkRule() @get:Rule val hiltRule = HiltAndroidRule(this) private val latch = CountdownLatch(1) @Inject lateinit var peopleRepository: PeopleRepository @Before fun setup() { hiltRule.inject() } @Test fun benchmarkSort() { benchmarkRule.measureRepeated { runBlocking { benchmarkRule.getStart().pauseTiming() withContext(Dispatchers.Main.immediate) { peopleRepository.peopleLiveData.observeForever(observer) } benchmarkRule.getStart().resumeTiming() peopleRepository.update() latch.await() assert(peopleRepository.peopleLiveData.value?.isNotEmpty() ?: false) } } } private val observer: Observer<List<Person>> = object : Observer<List<Person>> { override fun onChanged(people: List<Person>?) { peopleRepository.peopleLiveData.removeObserver(this) latch.countDown() } } }
Java
@RunWith(AndroidJUnit4.class) @HiltAndroidTest public class PeopleRepositoryBenchmark { @Rule public BenchmarkRule benchmarkRule = new BenchmarkRule(); @Rule public HiltAndroidRule hiltRule = new HiltAndroidRule(this); private CountdownLatch latch = new CountdownLatch(1); @Inject JavaPeopleRepository peopleRepository; @Before public void setup() { hiltRule.inject(); } @Test public void benchmarkSort() { BenchmarkRuleKt.measureRepeated(benchmarkRule, (Function1<BenchmarkRule.Scope, Unit>) scope -> { benchmarkRule.getState().pauseTiming(); new Handler(Looper.getMainLooper()).post(() -> { awaitValue(peopleRepository.getPeopleLiveData()); }); benchmarkRule.getState().resumeTiming(); peopleRepository.update(); try { latch.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } assert (!peopleRepository.getPeopleLiveData().getValue().isEmpty()); return Unit.INSTANCE; }); } private <T> void awaitValue(LiveData<T> liveData) { Observer<T> observer = new Observer<T>() { @Override public void onChanged(T t) { liveData.removeObserver(this); latch.countDown(); } }; liveData.observeForever(observer); return; } }
O exemplo anterior cria regras para o comparativo de mercado e o Hilt.
benchmarkRule realiza a medição do tempo do comparativo de mercado. hiltRule realiza a
injeção de dependência na classe de teste de comparativo de mercado. Invoque o método
inject() da regra do Hilt em uma função @Before para realizar a
injeção antes de executar testes individuais.
O comparativo de mercado pausa a medição de tempo enquanto o observador LiveData está
registrado. Em seguida, ele usa um latch para aguardar até que o LiveData seja atualizado antes de
concluir. Como a classificação é executada no tempo entre o momento em que
peopleRepository.update() é chamado e o momento em que LiveData recebe uma atualização,
a duração da classificação é incluída no tempo de comparativo de mercado.
Executar o Microbenchmark
Execute o comparativo de mercado com ./gradlew :benchmark:connectedBenchmarkAndroidTest
para realizar o comparativo de mercado em várias iterações e mostrar os dados de tempo no
Logcat:
PeopleRepositoryBenchmark.log[Metric (timeNs) results: median 613408.3952380952, min 451949.30476190476, max 1412143.5142857144, standardDeviation: 273221.2328680522...
O exemplo anterior mostra o resultado do comparativo de mercado entre 0,6 ms e 1,4 ms para executar o algoritmo de classificação em uma lista de 1.000 itens. No entanto, se você incluir a chamada de rede no comparativo de mercado, a variação entre as iterações será maior do que o tempo que a própria classificação leva para ser executada. Por isso, é necessário isolar a classificação da chamada de rede.
É sempre possível refatorar o código para facilitar a execução da classificação de forma isolada, mas, se você já estiver usando o Hilt, poderá usá-lo para injetar simulações para comparação.