Usando koin como herramienta de inyección de Android

etiquetas:

koin proporciona una interfaz API fácil de usar para Android , lo que le permite acceder fácilmente al marco de trabajo de koin.

[configuración gradle de koin en Android]

mp.weixin.qq.com/s/bscC7mO4O…

1. startKoin en la clase Aplicación

Desde su clase, puede usar la función e inyectar el contexto de Android así:

Application startKoin androidContext
class MainApplication : Application() 
override fun onCreate() 
    super.onCreate()

    startKoin 
        // Log Koin into Android logger
        androidLogger()
        // Reference Android context
        androidContext(this@MainApplication)
        // Load modules
        modules(myAppModules)

Si necesita iniciar Koin desde otra clase de Android, puede usar esta función para proporcionar su instancia de Android de la siguiente manera:startKoin Context

startKoin 
    //inject Android context
    androidContext(/* your android context */)
    // ...

2. Configuración adicional

Desde tu configuración de Koin (en código de bloque), también puedes configurar varias partes de Koin.startKoin

2.1 Registro de Koin para Android

koin proporciona una implementación de Android de log.

startKoin 
    // use Android logger - Level.INFO by default
    androidLogger()
    // ...

2.2 Cargar propiedades

Puede usar las propiedades de Koin para almacenar valores/claves en el archivo: assets/koin.properties

startKoin 
    // ...
    // use properties from assets/koin.properties
    androidFileProperties()

3. Inyectar instancia de objeto en Android

3.1 Preparándose para la clase de Android

koin proporciona KoinComponentsextensiones, que están disponibles para los componentes de Android, incluidosActivity Fragment Service ComponentCallbacks

Puede acceder a las extensiones de Kotlin de la siguiente manera:

by inject()- Instancia de computación perezosa del contenedor Koin

get()- Obtenga una instancia del contenedor Koin

Podemos declarar una propiedad para ser inyectada perezosamente:

module 
    // definition of Presenter
    factory  Presenter() 

clase DetailActivity: AppCompatActivity ()

// Lazy inject Presenter
override val presenter : Presenter by inject()

override fun onCreate(savedInstanceState: Bundle?) 
    //...

O podemos obtener una instancia directamente:

override fun onCreate(savedInstanceState: Bundle?) 
    super.onCreate(savedInstanceState)
// Retrieve a Presenter instance
val presenter : Presenter = get()

Nota: Si su clase no está extendida, simplemente agregue la interfaz KoinComponent si necesita una instancia de otra clase. inyectar() obtener()

3.2 Uso del contexto de Android

class MainApplication : Application() 
override fun onCreate() 
    super.onCreate()

    startKoin 
        //inject Android context
        androidContext(this@MainApplication)
        // ...

En su definición, la siguiente función le permite obtener instancias en el módulo Koin para ayudarlo a escribir fácilmente expresiones que requieren instancias.androidContext() androidApplication() Context Application

val appModule = module 
// create a Presenter instance with injection of R.string.mystring resources from Android
factory 
    MyPresenter(androidContext().resources.getString(R.string.mystring))

4. Constructores de DSL para Android

4.1 constructores de DSL

Koin ahora proporciona una nueva palabra clave DSL que le permite dirigirse directamente a los constructores de clases y evitar escribir sus definiciones en expresiones lambda.

Para Android, esto significa las siguientes palabras clave de DSL del nuevo constructor:

viewModelOf()- equivalente aviewModel

fragmentOf()- equivalente afragment

workerOf()- equivalente aworker

Nota: asegúrese de usar antes del nombre de la clase para ubicar el constructor de la clase:

4.2 Ejemplo de función DSL de Android

Dada una aplicación Android con los siguientes componentes:

// A simple service
class SimpleServiceImpl() : SimpleService

// un presentador, que usa SimpleService y puede recibir "id" clase de parámetro inyectada
FactoryPresenter (id de valor: cadena, servicio de valor: Servicio simple)

// un ViewModel que puede recibir el parámetro inyectado "id", use SimpleService y obtenga
la clase SavedStateHandle SimpleViewModel (val id: String, val service: SimpleService, val handle: SavedStateHandle) : ViewModel()

// una sesión con ámbito, que puede recibir un enlace a la
sesión de clase MyActivity (desde el ámbito) (actividad val: MyActivity)

// un Worker, usando SimpleService y obteniendo Context & WorkerParameters
class SimpleWorker(
private val simpleService: SimpleService,
appContext: Context,
private val params: WorkerParameters
) : CoroutineWorker(appContext, params)

Podemos declararlos así:

module 
    singleOf(::SimpleServiceImpl) bind<SimpleService>() 
factoryOf(::FactoryPresenter)

viewModelOf(::SimpleViewModel)

scope&lt;MyActivity&gt;()
    scopedOf(::Session)


workerOf(::SimpleWorker)

5. Uso de varios módulos de koin en Android

Al usar Koin, puede describir definiciones en módulos. En esta sección, veremos cómo declarar, organizar y vincular módulos.

Módulo múltiple de 5.1 koin

Los componentes no tienen que estar en el mismo módulo. Los módulos son espacios lógicos que lo ayudan a organizar definiciones y pueden depender de otros módulos de definición. Las definiciones son perezosas y solo se resuelven cuando un componente lo solicita.

Tomemos un ejemplo donde los componentes vinculados están en módulos separados:

// ComponentB <- ComponentA
class ComponentA()
class ComponentB(val componentA : ComponentA)

val moduleA = módulo
// Singleton ComponentA
single ComponentA()

val moduleB = módulo
// Singleton ComponentB con instancia vinculada ComponentA
single ComponentB(get())

Solo necesitamos declarar la lista de módulos usados ​​al iniciar el contenedor Koin:

class MainApplication : Application() 
override fun onCreate() 
    super.onCreate()

    startKoin 
        // ...

        // Load modules
        modules(moduleA, moduleB)

5.2 Contenido del módulo

Una nueva función está disponible en la clase que le permite combinar módulos al incluir otros módulos de manera organizada y estructuradaincludes() Module

El nuevo módulo tiene 2 características destacadas:

Divida los módulos grandes en módulos más pequeños y más enfocados.

En proyectos modulares, le permite un control más preciso sobre la visibilidad del módulo (vea el ejemplo a continuación).

¿Como funciona? Tomemos algunos módulos, incluimos los módulos en: parentModule

// `:feature` module
val childModule1 = module 
    /* Other definitions here. */

val childModule2 = módulo
/* Otras definiciones aquí. */

val parentModule = el módulo
incluye (childModule1, childModule2)

// :appmódulos
startKoin del módulo (módulo principal)

Tenga en cuenta que no necesitamos establecer explícitamente todos los módulos: al incluir, todos los módulos declarados se cargarán automáticamente.

parentModule includes childModule1 childModule2 parentModule childModule1 childModule2

INFORMACIÓN: la carga de módulos ahora está optimizada para aplanar todos los gráficos de módulos y evitar definiciones de módulos duplicadas.

Finalmente, puede incluir múltiples módulos anidados o repetidos y Koin aplanará todos los módulos incluidos, eliminando los duplicados:

// :feature module
val dataModule = module 
    /* Other definitions here. */

val domainModule = módulo
/* Otras definiciones aquí. */

val featureModule1 = el módulo
incluye (domainModule, dataModule)

val featureModule2 = el módulo
incluye (domainModule, dataModule)

// :
clase de módulo de aplicación MainApplication : Application()

override fun onCreate() 
    super.onCreate()

    startKoin 
        // ...

        // Load modules
         modules(featureModule1, featureModule2)

Tenga en cuenta que todos los módulos se incluirán solo una vez:dataModule domainModule featureModule1 featureModule2

5.3 Android ViewModel 和 Navegación

El módulo Gradle presenta una nueva palabra clave DSL que se agrega para ayudar a declarar y vincular los componentes de ViewModel al ciclo de vida de los componentes de Android. La palabra clave también está disponible, lo que le permite declarar un ViewModel usando su constructor.koin-android viewModel singlefactory viewModelOf

val appModule = module 
// ViewModel for Detail View
viewModel  DetailViewModel(get(), get()) 

// or directly with constructor
viewModelOf(::DetailViewModel)

El componente declarado debe extender al menos la clase. Puede especificar cómo inyectar el constructor de una clase y usar esa función para inyectar dependencias.android.arch.lifecycle.ViewModel get()

Nota: la palabra clave ayuda a declarar la instancia de fábrica de ViewModel. Esta instancia será manejada por ViewModelFactory interno y volverá a adjuntar la instancia de ViewModel cuando sea necesario. También permitirá inyectar parámetros.viewModel viewModelOf

5.4 Inyectar modelo de vista

Use viewModel en el componente de Android,Activity Fragment Service

by viewModel()- Propiedades de delegado diferido para inyectar modelos de vista en propiedades

getViewModel()- Obtener la instancia del modelo de vista directamente

class DetailActivity : AppCompatActivity() 
// Lazy inject ViewModel
val detailViewModel: DetailViewModel by viewModel()

5.5 Modelo de vista compartida de actividad

Una instancia de ViewModel se puede compartir entre un fragmento y su actividad principal.

Para inyectar el modelo de vista compartido usando: Fragmento

by activityViewModel()- Propiedades delegadas perezosas para inyectar instancias de viewModel compartidas en propiedades

get ActivityViewModel()- Obtener la instancia de viewModel compartida directamente

Simplemente declare el modelo de vista una vez:

val weatherAppModule = module 
// WeatherViewModel declaration for Weather View components
viewModel  WeatherViewModel(get(), get()) 

Nota: los calificadores de viewModel se tratarán como etiquetas de viewModel

y reutilizarlo en Actividad y Fragmento:

class WeatherActivity : AppCompatActivity() 
/*
 * Declare WeatherViewModel with Koin and allow constructor dependency injection
 */
private val weatherViewModel by viewModel&lt;WeatherViewModel&gt;()

clase WeatherHeaderFragment: Fragmento()

/*
 * Declare shared WeatherViewModel with WeatherActivity
 */
private val weatherViewModel by activityViewModel&lt;WeatherViewModel&gt;()

clase WeatherListFragment : Fragment()

/*
 * Declare shared WeatherViewModel with WeatherActivity
 */
private val weatherViewModel by activityViewModel&lt;WeatherViewModel&gt;()

5.6 Pasando argumentos a constructores

Pase los parámetros a viewModel, el código de muestra es el siguiente:

en el modulo

val appModule = module 
// ViewModel for Detail View with id as parameter injection
viewModel  parameters -&gt; DetailViewModel(id = parameters.get(), get(), get()) 
// ViewModel for Detail View with id as parameter injection, resolved from graph
viewModel  DetailViewModel(get(), get(), get()) 
// or Constructor DSL
viewModelOf(::DetailViewModel)

Parámetros entrantes del punto de inyección de dependencia

class DetailActivity : AppCompatActivity() 
val id : String // id of the view

// Lazy inject ViewModel with id parameter
val detailViewModel: DetailViewModel by viewModel parametersOf(id)

5.7 Inyección de manejo de estado guardado

Agregue nuevas propiedades escritas en el constructor para manejar el estado de ViewModel:SavedStateHandle

class MyStateVM(val handle: SavedStateHandle, val myService : MyService) : ViewModel()En el módulo Koin, simplemente analícelo con el parámetro o: get()

viewModel MyStateVM(get(), get()) o use el constructor DSL:

viewModelOf(::MyStateVM)En fragmento de actividad

by viewModel()- Propiedades de delegado diferido para inyectar instancias de modelo de vista de estado en propiedades

getViewModel()- Obtenga la instancia del modelo de vista de estado directamente

class DetailActivity : AppCompatActivity() 
// MyStateVM viewModel injected with SavedStateHandle
val myStateVM: MyStateVM by viewModel()

5.8 ViewModel en navegación Diagrama de navegación

Puede limitar las instancias de ViewModel al gráfico de navegación. Simplemente pase la identificación aby koinNavGraphViewModel()

class NavFragment : Fragment() 
val mainViewModel: NavViewModel by koinNavGraphViewModel(R.id.my_graph)

API común de 5.9 viewModel

Koin proporciona algunas API de "bajo nivel" para ajustar directamente su instancia de ViewModel.viewModelForClass ComponentActivity Fragment

ComponentActivity.viewModelForClass(
    clazz: KClass<T>,
    qualifier: Qualifier? = null,
    owner: ViewModelStoreOwner = this,
    state: BundleDefinition? = null,
    key: String? = null,
    parameters: ParametersDefinition? = null,
): Lazy<T>

También se proporciona una función de nivel superior:

fun <T : ViewModel> getLazyViewModelForClass(
    clazz: KClass<T>,
    owner: ViewModelStoreOwner,
    scope: Scope = GlobalContext.get().scopeRegistry.rootScope,
    qualifier: Qualifier? = null,
    state: BundleDefinition? = null,
    key: String? = null,
    parameters: ParametersDefinition? = null,
): Lazy<T>

5.10 API ViewModel - Compatibilidad con Java

La compatibilidad con Java debe agregarse a las dependencias:

// Java Compatibility
implementation "io.insert-koin:koin-android-compat:$koin_version"
您可以使用以下函数或静态函数将 ViewModel 实例注入到 Java 代码库中:viewModel() getViewModel() ViewModelCompat

@JvmOverloads
@JvmStatic
@MainThread
fun <T : ViewModel> getViewModel(
propietario: ViewModelStoreOwner,
clazz: Class<T>,
calificador: ¿Calificador? = nulo,
parámetros: Definición de parámetros? = nulo
)

6. Inyectar en Jetpack Compose

Infórmate primero sobre Jetpack Compose:

desarrollador.android.com/jetpack/com…

6.1 Inyectando @Composable

Al escribir funciones componibles, puede acceder a las siguientes API de Koin:

get()- Obtenga una instancia del contenedor Koin

getKoin()- Obtenga la instancia actual de Koin

Para un módulo que declara el componente "MyService":

val androidModule = module 
single  MyService() 

Podemos obtener su instancia de esta manera:

@Composable
fun App() 
    val myService = get<MyService>()

Nota: Para mantener la coherencia en la funcionalidad de Jetpack Compose, es mejor escribirlo para inyectar la instancia directamente en el atributo de función. Este enfoque permite usar Koin para la implementación predeterminada, pero lo deja abierto para inyectar instancias según sea necesario.

@Composable
fun App(myService: MyService = get()) 

6.2 vistaModelo @Composable

De la misma manera que accede a una instancia clásica singleton/factory, puede acceder a las siguientes API de Koin ViewModel:

getViewModel()o - obtener instanciakoinViewModel()

Para el módulo que declara el componente "MyViewModel":

module 
    viewModel  MyViewModel() 
    // or constructor DSL
    viewModelOf(::MyViewModel)

Podemos obtener su instancia de esta manera:

@Composable
fun App() 
    val vm = koinViewModel<MyViewModel>()

Podemos obtener su instancia en el parámetro de función:

@Composable
fun App(vm : MyViewModel = koinViewModel()) 

7. Administrar ámbitos de Android

Los componentes de Android, por ejemplo Activity、Fragment、Service, tienen un ciclo de vida, el sistema crea una instancia de estos componentes y hay devoluciones de llamadas de ciclo de vida correspondientes en los componentes.

Debido a que los componentes de Android tienen atributos de ciclo de vida, las instancias de componentes no se pueden pasar a koin. Según la duración del ciclo de vida, los componentes se pueden dividir en tres categorías:

  • • Componente de larga duración ( Service、database): utilizado por múltiples pantallas, nunca descartado
  • • Componentes de período medio ( User session): utilizados por múltiples pantallas y deben eliminarse después de un período de tiempo
  • • Componentes de corta duración ( ViewModel)--utilizados por una sola pantalla y deben eliminarse al final de la pantalla

Para componentes a largo plazo, generalmente usamos single para crear una instancia única globalmente en la aplicación

En el modo de arquitectura MVP, Presenter es un componente de ciclo corto

La forma de crearlo en Actividad es la siguiente

class DetailActivity : AppCompatActivity() 
// injected Presenter
override val presenter : Presenter by inject()

También podemos crear en módulo

Usamos el alcance de fábrica para crear la instancia de Presenter

val androidModule = module 
// Factory instance of Presenter
factory  Presenter() 

Generar un ámbito de instancia vinculado al ámbito

val androidModule = module 
scope&lt;DetailActivity&gt; 
    scoped  Presenter() 

La mayoría de las fugas de memoria de Android provienen de referencias a componentes de UI/Android de componentes que no son de Android. El sistema mantiene referencias a él y no puede reclamarlo por completo a través de la recolección de elementos no utilizados.

7.1 Declarar alcance de Android

Para calificar una dependencia en un componente de Android, debe declarar un alcance usando un bloque como este: alcance

class MyPresenter()
class MyAdapter(val presenter : MyPresenter)

módulo
// Declarar el alcance de MyActivity
scope<MyActivity>
// obtener la instancia de MyPresenter del alcance actual
scoped MyAdapter(get())
scoped MyPresenter()

7.2 Clase de alcance de Android

Koin proporciona clases de alcance relacionadas con los componentes del ciclo de vida de AndroidScopeActivity Retained ScopeActivity ScopeFragment

class MyActivity : ScopeActivity() 
// MyPresenter is resolved from MyActivity's scope
val presenter : MyPresenter by inject()

Android Scope debe usarse con interfaces para implementar campos como este:AndroidScopeComponent scope

abstract class ScopeActivity(
    @LayoutRes contentLayoutId: Int = 0,
) : AppCompatActivity(contentLayoutId), AndroidScopeComponent 
override val scope: Scope by activityScope()

override fun onCreate(savedInstanceState: Bundle?) 
    super.onCreate(savedInstanceState)

    checkNotNull(scope)

Necesitamos usar interfaces e implementar propiedades. Esto establecerá el Ámbito predeterminado utilizado por la clase.AndroidScopeComponent scope

7.3 Interfaz de alcance de Android

Para crear un alcance de Koin vinculado a un componente de Android, simplemente use la siguiente función:

createActivityScope()- Crear un Alcance para la Actividad actual (la parte del Alcance debe ser declarada)

createActivityRetainedScope()- Cree un Alcance retenido (compatible con ViewModel Lifecycle) para la Actividad actual (la parte del Alcance debe declararse)

createFragmentScope()- Cree un Ámbito para el Fragmento actual y vincúlelo al Ámbito de actividad principal. Estas funciones se pueden utilizar como delegados para implementar diferentes tipos de ámbitos:

activityScope()- Crear un Alcance para la Actividad actual (la parte del Alcance debe ser declarada)

activityRetainedScope()- Cree un Alcance retenido (compatible con ViewModel Lifecycle) para la Actividad actual (la parte del Alcance debe declararse)

fragmentScope()- Cree un Ámbito para el Fragmento actual y vincúlelo al Ámbito de actividad principal

class MyActivity() : AppCompatActivity(contentLayoutId), AndroidScopeComponent 
override val scope: Scope by activityScope()

También podemos establecer el alcance de retención (impulsado por el ciclo de vida de ViewModel) usando lo siguiente:

class MyActivity() : AppCompatActivity(contentLayoutId), AndroidScopeComponent 
override val scope: Scope by activityRetainedScope()

Si no desea usar la clase Scope de Android, puede usar su propia clase y crear una API con ScopeAndroidScopeComponent

7.4 Enlace de alcance

Los enlaces de ámbito permiten compartir instancias entre componentes con ámbitos personalizados. En un uso más amplio, puede usar instancias entre componentes. Por ejemplo, si necesitamos compartir una instancia.Scope UserSession

Primero declare una definición de alcance:

module 
    // Shared user session data
    scope(named("session")) 
        scoped  UserSession() 

Cuando sea el momento de comenzar a usar una instancia, cree un alcance para ella:UserSession

val ourSession = getKoin().createScope("ourSession",named("session"))

// vincular el ámbito de nuestra sesión al actual scope, desde ScopeActivity o ScopeFragment
scope.linkTo(nuestra sesión)

Luego utilízalo donde lo necesites:

class MyActivity1 : ScopeActivity() 
fun reuseSession()
    val ourSession = getKoin().createScope("ourSession",named("session"))

    // link ourSession scope to current `scope`, from ScopeActivity or ScopeFragment
    scope.linkTo(ourSession)

    // will look at MyActivity1's Scope + ourSession scope to resolve
    val userSession = get&lt;UserSession&gt;()

clase MyActivity2 : ScopeActivity()

fun reuseSession()
    val ourSession = getKoin().createScope("ourSession",named("session"))

    // link ourSession scope to current `scope`, from ScopeActivity or ScopeFragment
    scope.linkTo(ourSession)

    // will look at MyActivity2's Scope + ourSession scope to resolve
    val userSession = get&lt;UserSession&gt;()

8.Fábrica de fragmentos

Desde AndroidX ha lanzado una serie de paquetes para ampliar la funcionalidad de Androidandroidx.fragment Fragment

desarrollador.android.com/jetpack/y…

8.1 Fábrica de fragmentos

Desde el lanzamiento, se ha introducido una clase dedicada a la creación de instancias de clase: 2.1.0-alpha-3 FragmentFactoryFragmento

desarrollador.android.com/referencia/k…

Koin también proporciona un KoinFragmentFactoryFragmento de clase de fábrica para crear Fragmento

8.2 Configuración de fábrica de fragmentos

Primero, en KoinApplicationla declaración, establezca la instancia predeterminada usando la palabra clave: fragmentFactory()KoinFragmentFactory

 startKoin 
    // setup a KoinFragmentFactory instance
    fragmentFactory()
modules(...)

8.3 Declarar e inyectar fragmento

Declarar un Fragmento e inyectarlo en el módulo

class MyFragment(val myService: MyService) : Fragment() 

val appModule = módulo
único MyService()
fragmento MyFragment(get())

8.4 Obtener Fragmento

usar setupKoinFragmentFactory()la configuraciónFragmentFactory

Para consultar su Fragmento, usesupportFragmentManager

supportFragmentManager.beginTransaction()
            .replace<MyFragment>(R.id.mvvm_frame)
            .commit()

Agregar parámetros opcionales

supportFragmentManager.beginTransaction()
            .replace<MyFragment>(
                containerViewId = R.id.mvvm_frame,
                args = MyBundle(),
                tag = MyString()
            )

8.5 Fábrica de Fragmentos y Ámbitos Koin

Si desea utilizar el Ámbito de actividad de Koin, debe declarar su Fragmento como una definición en su Ámbito:scoped

val appModule = module 
    scope<MyActivity> 
        fragment  MyFragment(get()) 

Y configura tu Koin Fragment Factory con tu Scope:setupKoinFragmentFactory(lifecycleScope)

class MyActivity : AppCompatActivity() 
override fun onCreate(savedInstanceState: Bundle?) 
    // Koin Fragment Factory
    setupKoinFragmentFactory(lifecycleScope)

    super.onCreate(savedInstanceState)
    //...

9. Inyección Koin de WorkManager

koin proporciona un paquete de componentes separado koin-androidx-workmanager para WorkManager

Primero, en la declaración de KoinApplication, use la palabra clave para establecer una instancia de WorkManager personalizada:workManagerFactory()

class MainApplication : Application(), KoinComponent 
override fun onCreate() 
    super.onCreate()
    startKoin 
        // setup a WorkManager instance
        workManagerFactory()
        modules(...)
    
    setupWorkManagerFactory()

Modificación de AndroidManifest.xml para evitar usar el predeterminado

    <application . . .>
        . . .
        <provider
            android:name="androidx.work.impl.WorkManagerInitializer"
            android:authorities="$applicationId.workmanager-init"
            tools:node="remove" />
  </application>

9.1 Declarar ListenableWorker

val appModule = module 
single  MyService() 
worker  MyListenableWorker(get()) 

9.2 Crear WorkManagerFactory adicional

class MainApplication : Application(), KoinComponent 
override fun onCreate() 
    super.onCreate()

    startKoin 
       workManagerFactory(workFactory1, workFactory2)
       . . .
    

    setupWorkManagerFactory()

Si WorkManagerFactoryse pueden crear instancias tanto de Koin como de workFactory1 ListenableWorker, se utilizará la fábrica proporcionada por Koin.

9.3 Cambiar el manifiesto de koin lib en sí

Si koin-androidx-workmanagerla fábrica predeterminada está deshabilitada y el desarrollador de la aplicación no inicializa la infraestructura del administrador de trabajo de koin, terminará sin una fábrica de administrador de trabajo disponible.

En vista de la situación anterior, realizamos las siguientes mejoras de DSL:

val workerFactoryModule = module 
factory<WorkFactory>  WorkFactory1() 
factory<WorkFactory>  WorkFactory2() 

Entonces haz que koin internamente haga algo como

fun Application.setupWorkManagerFactory(
// no vararg for WorkerFactory
) 
. . .
getKoin().getAll<WorkerFactory>()
.forEach 
delegatingWorkerFactory.addFactory(it)

Link de referencia

insertar-koin.io/

Lectura recomendada

Autor: Calvin873
Enlace: https://juejin.cn/post/7189917106580750395

Supongo que te gusta

Origin blog.csdn.net/gqg_guan/article/details/131801957
Recomendado
Clasificación