Overview of Lich libraries
mastercom.linecorp.lich.repository·master·Indexed 41 days ago
https://github.com/line/lichA collection of lightweight Android libraries providing a Service Locator framework for managing singleton components. Lich simplifies component management, lifecycle handling, and networking (OkHttp/Thrift) without requiring configuration files, annotations, or DSLs. It features lazy acquisition via Kotlin delegated properties, support for Jetpack Compose, and multi-module dependency resolution using delegateCreation() and delegateToServiceLoader().
com.linecorp.lich.The Lich sample application is a multi-module project designed to demonstrate the use of Lich libraries within an MVVM (Model-View-ViewModel) architecture. It is organized into the following functional modules:
model-api.The sample_thrift directory contains a reference implementation of a Thrift protocol. This can be used to understand how to define services and data structures in Thrift and how the resulting Java code is structured within the Lich ecosystem.
src/main/thrift/FooService.thrift.src/main/java/com/linecorp/lich/sample/thrift/.AutoResetLifecycleScope is a safer alternative to AndroidX lifecycleScope. While lifecycleScope coroutines are only cancelled when the Lifecycle is DESTROYED, AutoResetLifecycleScope can be configured to automatically cancel coroutines when the Lifecycle is STOPPED (using ResetPolicy.ON_STOP).
This prevents resource leaks common when using lifecycleScope.launchWhenStarted to collect Flows, as those coroutines remain active even when the Lifecycle is stopped.
Key behaviors:
Dispatchers.Main.Lifecycle is DESTROYED.Lifecycle is STOPPED if ResetPolicy.ON_STOP is used.Fragment, do not use viewLifecycleOwner; use the Fragment instance itself.class FooFragment : Fragment() {
// Any coroutines launched from this scope are automatically cancelled when FooFragment is STOPPED.
// NOTE: Don't use `viewLifecycleOwner` here.
private val autoResetLifecycleScope: CoroutineScope = AutoResetLifecycleScope(this)
// A repository that provides some data as a Flow.
private val fooRepository = FooRepository()
override fun onStart() {
super.onStart()
// It is SAFE to collect a flow with AutoResetLifecycleScope.
// The coroutine launched here will be automatically cancelled just before `FooFragment.onStop()`.
autoResetLifecycleScope.launch {
fooRepository.dataFlow().collect { value ->
textView.text = value
}
}
}
}Lich provides several specialized libraries for Android development:
component library.Lich is a Service Locator framework for managing singleton components on Android. It is not a Dependency Injection (DI) framework; it uses no configuration files, annotations (except AutoService), or DSLs. Instead, dependencies are defined programmatically by implementing a ComponentFactory in a class's companion object.
Key characteristics:
The AbstractViewModel class implements CoroutineScope and is bound to Dispatchers.Main. This scope is automatically cancelled just before onCleared() is called. You can launch coroutines directly using launch { ... } without needing to reference a specific scope variable, similar to AndroidX viewModelScope.
class BarViewModel(private val barRepository: BarRepository) : AbstractViewModel() {
val barText: MutableLiveData<String> = MutableLiveData("")
fun loadBarText() {
// The launched job will be automatically cancelled when this ViewModel is destroyed.
launch {
val barData = barRepository.loadBarData()
barText.value = barData.text
}
}
}If two components depend on each other (e.g., ComponentX needs ComponentY and vice versa), direct instantiation in the constructor will cause a circular dependency error.
To resolve this, use lazy acquisition by using the by context.component(Type) property delegate instead of calling context.getComponent(Type) inside the constructor. This defers the resolution of the dependency until it is actually accessed.
class ComponentX(context: Context) {
// Use 'by context.component' for lazy resolution
private val componentY by context.component(ComponentY)
companion object : ComponentFactory<ComponentX>() {
override fun createComponent(context: Context): ComponentX = ComponentX(context)
}
}
class ComponentY(context: Context) {
private val componentX by context.component(ComponentX)
companion object : ComponentFactory<ComponentY>() {
override fun createComponent(context: Context): ComponentY = ComponentY(context)
}
}In multi-module projects where a 'base' module needs to access features in a 'foo' module without a direct dependency, you can use the Facade pattern combined with delegateCreation().
ComponentFactory, use delegateCreation(context, "full.package.path.ToFactory") to point to the implementation factory.DelegatedComponentFactory with a public empty constructor. This class is responsible for instantiating the actual implementation.// "base" module
interface FooModuleFacade {
fun launchFooActivity()
companion object : ComponentFactory<FooModuleFacade>() {
override fun createComponent(context: Context): FooModuleFacade =
delegateCreation(context, "module.foo.FooModuleFacadeFactory")
}
}
// "foo" module
class FooModuleFacadeFactory: DelegatedComponentFactory<FooModuleFacade>() {
override fun createComponent(context: Context): FooModuleFacade =
FooModuleFacadeImpl(context)
}
internal class FooModuleFacadeImpl(private val context: Context) : FooModuleFacade {
override fun launchFooActivity() { /* ... */ }
}Add the following dependency to your build.gradle file to use coroutine-aware extensions for OkHttp:
// build.gradle
dependencies {
implementation 'com.linecorp.lich:okhttp:x.x.x'
}implementation 'com.linecorp.lich:okhttp:x.x.x'Lich provides mockViewModel(ViewModelClass) to easily mock ViewModels in tests. This works with both MockK and Mockito-Kotlin.
Note: In Robolectric tests, all ViewModel mocks are automatically cleared. For other test types, you should call clearAllMockViewModels() in your @After block to prevent leakages between tests.
// MockK Example
@Test
fun testViewBinding() {
val mockBarText = MutableLiveData("Mocked.")
val mockViewModelHandle = mockViewModel(BarViewModel) {
every { barText } returns mockBarText
}
// ... use mockViewModelHandle.mock to interact with the mock
}
// Mockito-Kotlin Example
@Test
fun testViewBinding() {
val mockBarText = MutableLiveData("Mocked.")
val mockViewModelHandle = mockViewModel(BarViewModel) {
on { barText } doReturn mockBarText
}
// ...
}Lich can automatically generate ViewModelArgs classes to handle the boilerplate of initializing a SavedStateHandle from a Bundle.
Use @GenerateArgs on the ViewModel class and @Argument on the properties you want included in the generated arguments class.
@GenerateArgs
class FooViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
@Argument
private val userName: String by savedStateHandle.required()
@Argument(isOptional = true)
private val tags: Array<String> by savedStateHandle.initial(arrayOf("normal"))
@Argument
private var attachment: Parcelable? by savedStateHandle
@Argument
private val message: MutableLiveData<CharSequence> by savedStateHandle.liveData()
}The compiler generates a class (e.g., FooViewModelArgs) that implements ViewModelArgs. You can use this to pass arguments to a Fragment or Activity.
@GenerateArgs
class FooViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
@Argument
private val userName: String by savedStateHandle.required()
@Argument(isOptional = true)
private val tags: Array<String> by savedStateHandle.initial(arrayOf("normal"))
@Argument
private var attachment: Parcelable? by savedStateHandle
@Argument
private val message: MutableLiveData<CharSequence> by savedStateHandle.liveData()
}