Muitos apps usam o Hilt para injetar diferentes comportamentos em diversas variantes de build. Isso pode ser muito útil ao fazer uma comparação da microcomparação do seu app, porque permite você troca um componente que pode distorcer os resultados. Por exemplo, o seguinte O snippet de código 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 durante o comparativo de mercado, implemente uma chamada de rede falsa para obter um resultado mais preciso.
Incluir uma chamada de rede real durante o comparativo de mercado dificulta a interpretação resultados do comparativo de mercado. As chamadas de rede podem ser afetadas por muitos fatores externos. a duração deles pode variar entre as iterações de execução da comparação. A das chamadas de rede pode demorar mais do que a classificação.
Implementar uma chamada de rede falsa usando o Hilt
A chamada para dataSource.getPeople()
, conforme mostrado no exemplo anterior,
contém uma chamada de rede. No entanto, a instância NetworkDataSource
é injetada
pelo Hilt. É possível substituí-lo pela seguinte implementação fictícia para
Comparativo de mercado:
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ê faz
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 comprimento possivelmente variável.
No entanto, isso é feito durante a inicialização e não causa irregularidades
quando getPeople()
é chamado durante a comparação.
Alguns apps já usam falsificações em builds de depuração para remover dependências de back-end. No entanto, é necessário comparar um build que esteja próximo ao de lançamento sempre que possível. O restante deste documento usa uma estrutura multimódulo e multivariante conforme descrito em Configuração completa do projeto.
Existem três módulos:
benchmarkable
: contém o código a ser comparado.benchmark
: contém o código de comparação.app
: contém o código restante do app.
Cada um dos módulos anteriores tem uma variante de build chamada benchmark
junto com
as variantes comuns debug
e release
.
Configurar o módulo de comparação
O código da chamada de rede falsa está no conjunto de origem debug
do
módulo benchmarkable
, e a implementação completa da rede está no release
conjunto de origem do mesmo módulo. O arquivo de recursos que contém os dados retornados por
a implementação fictícia está no conjunto de origem debug
para evitar sobrecarga do APK
o build release
. A variante benchmark
precisa ser baseada em release
e
use o conjunto de origem debug
. A configuração do build para a variante benchmark
do módulo benchmarkable
que contém a implementação fictícia é o 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 testes 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 o
classe HiltTestApplication
. Faça as seguintes alterações no build
configuração:
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 do Gradle necessários ao build.
- Especifica que o executor de testes personalizados é 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.
Você precisa mudar o testBuildType
para garantir que o Gradle crie a
connectedBenchmarkAndroidTest
, que realiza a comparação.
Criar a microcomparação
A comparação é implementada da seguinte forma:
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 a comparação e o Hilt.
benchmarkRule
executa a marcação de tempo da comparação. hiltRule
executa
injeção de dependência na classe de teste de comparação. Você precisa invocar o
Método inject()
da regra do Hilt em uma função @Before
para executar
antes de executar qualquer teste individual.
A comparação pausa o tempo enquanto o observador LiveData
está
registrados. Em seguida, ele usa uma trava para aguardar até que o LiveData
seja atualizado antes
de preparação. Como a classificação é executada no tempo entre quando
peopleRepository.update()
é chamado e, quando LiveData
recebe uma atualização,
a duração da classificação é incluída na marcação de tempo da comparação.
Executar a microcomparação
Executar a comparação com ./gradlew :benchmark:connectedBenchmarkAndroidTest
para realizar a comparação ao longo de muitas iterações e exibir os dados de tempo
Logcat:
PeopleRepositoryBenchmark.log[Metric (timeNs) results: median 613408.3952380952, min 451949.30476190476, max 1412143.5142857144, standardDeviation: 273221.2328680522...
O exemplo anterior mostra o resultado da comparação entre 0,6 ms e 1,4 ms para execução o algoritmo de classificação em uma lista de mil itens. No entanto, se você incluir chamada de rede no comparativo de mercado, a variação entre as iterações será maior do que o tempo que a classificação leva para ser executada, ou seja, a necessidade de isolar a classificação da chamada de rede.
Sempre é possível refatorar o código para facilitar a execução da classificação mas se você já usa o Hilt, pode usá-lo para injetar falsificações para comparativos de mercado.