line / lich Goto Github PK
View Code? Open in Web Editor NEWA library collection that enhances the development of Android apps.
License: Apache License 2.0
A library collection that enhances the development of Android apps.
License: Apache License 2.0
In GitHub Actions, :viewmodel-test-mockk:connectedDebugAndroidTest
crashes frequently with the following messages.
> Task :viewmodel-test-mockk:connectedDebugAndroidTest
Starting 4 tests on test(AVD) - 10
com.linecorp.lich.viewmodel.test.mockk.MockingTest > mockViewModel[test(AVD) - 10] FAILED
Test failed to run to completion. Reason: 'Instrumentation run failed due to 'Process crashed.''. Check device logcat for details
Tests on test(AVD) - 10 failed: Instrumentation run failed due to 'Process crashed.'
> Task :viewmodel-test-mockk:connectedDebugAndroidTest FAILED
But, this crash cannot be reproduced on my Pixel devices. :(
After upgrading AGP from 7.1.2 to 7.2.2, gradlew connectedAndroidTest
fails with the following errors.
com.linecorp.lich.component.ActivityTest > activityLazy[Pixel_5_API_32(AVD) - 12] FAILED
java.lang.RuntimeException: Unable to resolve activity for: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.linecorp.lich.component.test/com.linecorp.lich.component.ComponentTestActivity }
at androidx.test.core.app.InstrumentationActivityInvoker.startActivity(InstrumentationActivityInvoker.java:402)
Tests on Pixel_5_API_32(AVD) - 12 failed: There was 1 failure(s).
In the current implementation, the DefaultComponentProvider
is not utilizing AutoService to register the ServiceLocator. Adding manual registration always error-prone if we have multiple ServiceLocator components. In order to automate the process, we can install AutoService
to DefaultComponentProvider
Potential changes from
internal class DefaultComponentProvider : ComponentProvider
to
@AutoService(ComponentProvider::class)
internal class DefaultComponentProvider : ComponentProvider {
On the latest snapshot of lich-thrift
at this moment, in ThriftRequestBody
, the content type of the request message is hard-coded like:, in ThriftRequestBody
, the content type of the request message is hard-coded like:
However, according to the Armeria document and the armeria API server's actual behavior,
application/x-thrift
uses the TBINARY
format as its protocol. And, if the protocol is changed, the content type of the request message should be changed as well according to the message's actual protocol.
Foo
's companion object extends ComponentFactory or ViewModelFactory, its type parameter must be Foo
.I'd like to have a lint checker for the above rule.
That is, the lint checker reports mismatches like this:
class Foo {
// snip...
companion object : ComponentFactory<Bar>() { // <- WRONG!!
override fun createComponent(context: Context): Bar = TODO()
}
}
Enabling DexGuard which causing exceptions on the lich component. Please see stack traces below.
Stack Traces
04-22 08:50:50.240 25666 25666 E AndroidRuntime: Caused by: com.linecorp.lich.component.FactoryDelegationException: Service implementation is not found.
04-22 08:50:50.240 25666 25666 E AndroidRuntime: at com.linecorp.lich.component.ComponentFactory.loadServiceLoaderComponent(Unknown Source:40)
04-22 08:50:50.240 25666 25666 E AndroidRuntime: at o.uhc$a.b(Unknown Source:20)
04-22 08:50:50.240 25666 25666 E AndroidRuntime: at o.uhc$a.createComponent(Unknown Source:10)
To fix the issue we need to add these rules which can be used for Proguard or Dexguard
-keep public class <class>.DefaultComponentProvider { *; }
-keep interface <class>.ComponentProvider { *; }
-keep public class * implements <class>.ServiceLoaderComponent { *; }
-if public class **.*$Companion extends <class>.ComponentFactory
-keep class <1>.<2> { *; }
It's a best practice to be cooperative to check the state of cancellable on suspendCancellableCoroutine.
Especially on this resumeWith and resumeWithException which manifested on OkHttpExtensions.kt.
The rationale of having cooperative implementation basically to make sure the invocation does not throw an exception such as CompletionHandlerException on the call-site.
from
suspend fun <T> OkHttpClient.call(request: Request, responseHandler: (Response) -> T): T =
suspendCancellableCoroutine { cont ->
val call = newCall(request)
cont.invokeOnCancellation { call.cancel() }
call.enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
val result = runCatching { response.use(responseHandler) }
cont.resumeWith(result)
}
override fun onFailure(call: Call, e: IOException) {
cont.resumeWithException(e)
}
})
}
to
suspend fun <T> OkHttpClient.call(request: Request, responseHandler: (Response) -> T): T =
suspendCancellableCoroutine { cont ->
val call = newCall(request)
cont.invokeOnCancellation { call.cancel() }
call.enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
if (cont.isActive) {
val result = runCatching { response.use(responseHandler) }
cont.resumeWith(result)
}
}
override fun onFailure(call: Call, e: IOException) {
if (cont.isActive) {
cont.resumeWithException(e)
}
}
})
}
Since mockito-kotlin 2.2.6, its namespace is renamed from com.nhaarman.mockitokotlin2
to org.mockito.kotlin
https://github.com/mockito/mockito-kotlin/releases/tag/2.2.6
So, if unittest project is using mockito-kotlin 2.2.6 or higher version,
lich can raise ClassNotFoundException
while perform unittest
Hello, I have an idea for lich/component. The following is the proposal for the idea.
This proposal is to suggest to add some new API for lich component. The new APIs will release the component object when given lifecycle takes ON_DESTROY
event, and then it will not be used anymore.
In now, the component created by lich is considered as singleton because ComponentFactory<T>
is defined by object
keyword. Also this implementation is guided in README.md
. But this will imply some problems.
context.getComponent
has some confused definition. This function call looks that the created component will receive the given context
instance. But the component will receive the applicationContext
.Thanksfully, there is a concept of Lifecycle
and ViewModelStore.clear()
in Android framework. I hope that the lifetime of the component is the same as the lifecycle of context.
I don't have the concrete implementation for this proposal. So this section is just a sketch to implement this proposal. After this proposal is approved, the concrete implementation should be followed.
The steps to implement this proposal is followings.
ComponentProvider
DefaultComponentProvider.getComponent
to observe lifecycle.To get the two types of owners, the ComponentProvider
is needed to be changed.
interface ComponentProvider {
fun <T : Any> getComponent(context: Context, factory: ComponentFactory<T>, owner: Any?) : T
}
New classes for releasing the component are needed.
LifecycleObserver
for releasing with the lifecycleViewModel
for releasing with the viewmodel lifecycle// A [ViewModel] for releasing components
// The components will be released with the [ViewModelStore.clear] call.
internal class ResetComponentViewModel(
private val factory: ComponentFactory<*>
): ViewModel() {
override fun onCleared() {
super.onCleared()
ComponentFactory.Accessor.setComponent(factory, null)
}
internal class Factory(
private val factory: ComponentFactory<*>
): ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return ResetComponentViewModel(factory) as T
}
}
}
// A [LifecycleObserver] for releasing components
// The components will be released with [ON_DESTROY] event.
internal class ResetComponentLifecycleObserver(
private val factory: ComponentFactory<*>
): DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
ComponentFactory.Accessor.setComponent(factory, null)
}
}
ComponentProvider.getComponent
ComponentProvider
should observe the given lifecycle
using the context
.
internal class DefaultComponentProvider : ComponentProvider {
override fun <T : Any> getComponent(context: Context, factory: ComponentFactory<T>, owner: Any?): T {
val accessor = ComponentFactory.Accessor
accessor.getComponent(factory)?.let {
@Suppress("UNCHECKED_CAST")
return if (it is Creating) it.await() else it as T
}
val creating = Creating()
while (!accessor.compareAndSetComponent(factory, null, creating)) {
accessor.getComponent(factory)?.let {
@Suppress("UNCHECKED_CAST")
return if (it is Creating) it.await() else it as T
}
}
// Pass the given context using lifecycle
val componentContext = when(owner) {
is ViewModelStoreOwner -> context.applicationContext
is LifecycleOwner -> context
else -> context.applicationContext
}
val result = runCatching { accessor.createComponent(factory, componentContext) }
creating.setResult(result)
accessor.setComponent(factory, result.getOrNull())
// observe lifecycle
when(owner) {
is ViewModelStoreOwner -> {
val viewModelFactory = ComponentFactory.ResetComponentViewModel.Factory(factory)
ViewModelProvider(owner, viewModelFactory)
.get<ComponentFactory.ResetComponentViewModel>()
}
is LifecycleOwner -> {
owner.lifecycle.addObserver(
ComponentFactory.ResetComponentLifecycleObserver(factory)
)
}
}
return result.getOrThrow()
}
}
From the Components.kt
, the entry point of this new features should be provided.
@JvmName("get")
fun <T : Any> Context.getComponent(factory: ComponentFactory<T>): T = when(this) {
// component is bounded to the lifecycle of ViewModel
is ViewModelStoreOwner -> componentProvider.getComponent(applicationContext, factory, this)
// component is bounded to the lifecycle of the android component
is LifecycleOwner -> componentProvider.getComponent(this, factory, this)
// component is singleton
else -> componentProvider.getComponent(applicationContext, factory, null)
}
// This API will be used for the ViewModelProvider.Factory.
// The extras[APPLICATION_KEY] is application object so there is no way to determine whether the given context is bounded to lifecylce.
// So a new API will be needed to support components in ViewModel.
@JvmName("get")
fun <T: Any> CreationExtras.getComponent(factory: ComponentFactory<T>): T {
val context = requireNotNull(this[APPLICATION_KEY])
val viewModelStoreOwner = requireNotNull(this[VIEW_MODEL_STORE_OWNER_KEY])
return componentProvider.getComponent(context, factory, viewModelStoreOwner)
}
If an application is using Lich-component, there is no things to change immediately. The public APIs are not changed, so it is okay to use lich-component, if there is no need to optimize memory. But:
ViewModelProvider.Factory
to use CreationExtras.getComponent
to handle the lifecycle of components in ViewModel
.context.getComponent
calls to context.applicationContext.getComponent
manually.The remained problems of the above change are considered as followings.
context
object might be leaked. The developers should use applicationContext
for multiple lifecycle-aware Android components.If you have an idea, please let me know.
class FooViewModel(savedState: SavedState) : AbstractViewModel() {
private val fooArg: String by savedState.required()
}
In Kotlin 1.3.70, compilation for the above code fails with an exception like this:
e: java.lang.IllegalStateException: Couldn't obtain compiled function body for
public final inline operator fun provideDelegate(thisRef: kotlin.Any?,
property: kotlin.reflect.KProperty<*>): com.linecorp.lich.viewmodel.InitializedSavedState<T>
defined in com.linecorp.lich.viewmodel.RequiringSavedStateDelegate[DeserializedSimpleFunctionDescriptor@72dd0f7d]
This seems to be due to a bug where the provideDelegate
function does not work well with inline classes.
https://kotlinlang.org/docs/reference/delegated-properties.html#providing-a-delegate-since-11
https://kotlinlang.org/docs/reference/inline-classes.html
androidx.fragment:fragment-ktx:1.4.0
depends on androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1
, and
androidx.lifecycle:lifecycle-viewmodel-savedstate:2.4.0
depends on androidx.lifecycle:lifecycle-viewmodel:2.4.0
.
But, lifecycle-viewmodel-ktx:2.3.1
and lifecycle-viewmodel:2.4.0
contain some classes with the same names, which leads to compilation errors.
Therefore, as a workaround, we explicitly added dependencies to androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0
.
Related issue : google/ksp#746
When using ksp and savedstate compiler, IDE(such as Android Studio) cannot resolve symbols from generated sources as default.
In this case, many IDE feature will be malfunction such as Code Formatting
In other libraries cases such as Room
which are providing annotation processors, library provides Builder
API which are wrapping generated sources and allow to access.
ex : https://developer.android.com/training/data-storage/room#usage
Similar to Room
, is it possible to avoid referencing the generated code directly?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.