Aprenda como utilizar o ContentProvider corretamente

Vocês já pararam para pensar qual é a primeira coisa que nós temos que fazer após importarmos uma lib? Inicializá-la com contexto em uma classe Application. Se você já precisou utilizar alguma lib do Firebase, você vai se perguntar: Opa, eu nunca tive que fazer isso.

Vamos para nosso estudo de caso.

Estudo de caso

Atualmente, estamos desenvolvendo um aplicativo que utiliza Koin, Stetho e PreferenceCache. Até algumas semanas tínhamos uma classe chamada AppTemplateApplication que era responsável por inicializar nossas libs e era dessa forma

class AppTemplateApplication : Application() {

    override fun onCreate() {
        super.onCreate()
        PreferencesCache.init(this)
        if(BuildConfig.DEBUG) Stetho.initializeWithDefaults(this)
        startKoin(this, appTemplateModules)
    }
}

E essa classe precisa ser declarada em nosso AndroidManifest.xml para ser executada:

<application
    android:name=".AppTemplateApplication"
   ...
>

Imagine se fossemos adicionando mais libs ao longo da execução do projeto… Teríamos que adicionar cada inicialização em nossa AppTemplateApplication. Ou seja, em nosso projeto teríamos que sobrescrever uma Application() somente para inicializar libs.


Uma bazooka para resolver um problema 🙂

Manifest-Merger

De acordo com a documentação do android:

Seu arquivo APK pode conter apenas um arquivo AndroidManifest.xml, mas o projeto do Android Studio pode conter vários - fornecidos pelo main source set, built variants e libs importadas. Portanto, ao criar seu aplicativo, a compilação do Gradle mescla todos os arquivos de manifesto em um único arquivo de manifesto que é empacotado no seu APK.
A ferramenta de merge combina todos os elementos XML de cada arquivo, seguindo algumas heurísticas de merge e obedecendo às preferências de merge que você definiu.

E o porque estamos falando de Manifest-Merger? Se dermos uma olhada em nosso AndroidManifest, iremos descobrir como algumas libs conseguem fazer sua inicialização sem a interferência do programador que irá utilizá-la…

Em nosso Estudo de caso iremos investigar como o Firebase faz essa magia. No final do nosso AndroidManifest após o processo de merge, podemos ver o import de um provider

<provider
  android:name="com.google.firebase.provider.FirebaseInitProvider"
  android:authorities="APPTEMPLATE.firebaseinitprovider"
  android:exported="false"
  android:initOrder="100" />

Se investigarmos a classe FirebaseInitProvider poderemos ver que esse provider tem acesso ao contexto da aplicação usando this.getContext()

public boolean onCreate() {
    if (FirebaseApp.initializeApp(this.getContext()) == null) {
        Log.i("FirebaseInitProvider", "FirebaseApp initialization unsuccessful");
    } else {
        Log.i("FirebaseInitProvider", "FirebaseApp initialization successful");
    }
    return false;
}

Mas se analisarmos essa classe poderemos ver que o fato dela extender a ContentProvider a obriga implementar os métodos insert, query, onCreate, update, delete, getType.

DefaultProvider

Como vimos, a necessidade de implementar todos os métodos citados acaba tornando nosso Provider longo… Para mitigarmos esse problema, utilizaremos um DefaultProvider, uma open class.

open class DefaultProvider : ContentProvider() {
    override fun insert(uri: Uri?, values: ContentValues?): Uri { TODO("not implemented") }

    override fun query(uri: Uri?, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor { TODO("not implemented") }

    override fun onCreate(): Boolean { TODO("not implemented") }

    override fun update(uri: Uri?, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int {TODO("not implemented")}

    override fun delete(uri: Uri?, selection: String?, selectionArgs: Array<out String>?): Int { TODO("not implemented")}

    override fun getType(uri: Uri?): String { TODO("not implemented") }
}

A partir disso, iremos fazer com que nosso provider estenda-o.

KoinProvider, StethoProvider, CacheProvider

Diante do exposto, iremos construir nossos providers.

class KoinInitProvider : DefaultProvider() {
    override fun onCreate(): Boolean {
        context?.let { startKoin(it, appTemplateModules) }
        return true
    }
}
class PreferenceCacheInitProvider : DefaultProvider() {
    override fun onCreate(): Boolean {
        context?.let { PreferencesCache.init(it) }
        return true
    }
}
class StethoInitProvider : DefaultProvider() {
override fun onCreate(): Boolean {
        if (BuildConfig.DEBUG) context?.let {     Stetho.initializeWithDefaults(it) }
        return true
    }
}

e em nosso AndroidManifest.xml

<provider
 android:name=".provider.koin.KoinInitProvider"
 android:authorities=".provider.koin.KoinInitProvider" />

<provider
 android:name=".provider.cache.PreferenceCacheInitProvider"
 android:authorities=".provider.cache.PreferenceCacheInitProvider" />

<provider
 android:name=".provider.stetho.StethoInitProvider"
 android:authorities="com.facebook.stetho.StethoInitProvider" />

Conclusão

Bom, com a utilização do ContentProvider podemos eliminar a nossa classe AppTemplateApplication.class e utilizar um recurso do Android para inicializarmos as libs que o projeto utiliza.
Além da retirada da AppTemplateApplication, notamos que todos os providers utilizam o null safety do Kotlin para atribuir o contexto a inicialização das libs.

Esse artigo surgiu de um papo de dois devs paixão, valeu Matheus Kreuz Bristot pelo café e por essa abstração show.

Ps: Quer trocar uma ideia sobre o artigo ou sobre desenvolvimento mobile? Deixa um comentário show, que vai ser um prazer responde-lo.

Texto por: Caíque Minhare e Matheus Bristot