| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
This repo contains the CovPass app and commonly needed modules for Kotlin + Android.
The most important modules are:
The apps live in these modules:
Note: We explicitly avoid using flavors because they are problematic in many ways. They cause unnecessary Gradle scripts complexity for even non-trivial customizations, they interact badly with module substitution, the variant switcher doesn't work properly in all situations, switching flavors takes time (whereas apps in modules can be switched and launched directly), etc. Our experience at IBM has been much smoother since we threw away all flavors and switched to using modules.
We publish stable releases in separate release/... branches (e.g. release/1.9.x). Pick the latest release branch to get the latest officially published app version.
The main branch contains the development state up to the latest stable release branch. Often this branch is a little bit behind the latest stable release branch due to our release policy which requires manual reviews and approvals by legal and other teams before we get the green light to publish source into the open.
You'll need a GitHub personal access token with read:packages access and in the parent folder above your repository create a file called covpass.properties:
githubUsername=github-username githubPassword=personalaccesstoken
To actually build the app:
To fetch the latest DSC list:
./gradlew downloadDscList
Currently we publish to GitHub Packages. You can integrate the SDK like this via Gradle:
repositories {
maven {
url "https://maven.pkg.github.com/Digitaler-Impfnachweis/*"
credentials {
username githubUsername
password githubPassword
}
}
maven {
url "https://maven.pkg.github.com/ehn-dcc-development/*"
credentials {
username githubUsername
password githubPassword
}
}
}
// Enable core lib desugaring for java.time support (no need for threeten).
// This requires at least JavaVersion.VERSION_8, but if you use
// Android Gradle Plugin 7.0.0 you can use VERSION_11.
def javaVersion = JavaVersion.VERSION_11
android {
compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility javaVersion
targetCompatibility javaVersion
}
kotlinOptions {
jvmTarget = javaVersion.toString()
}
}
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
// Add our BOM, so you get the right version constraints.
api platform("com.ibm.health.de.rki.covpass:covpass-bom:$covpassVersion")
// Add the SDK (without version number - that's in the BOM already).
api "com.ibm.health.de.rki.covpass:covpass-sdk"
// We also need these security providers.
api 'org.conscrypt:conscrypt-android'
api "org.bouncycastle:bcprov-jdk15to18"
api "org.bouncycastle:bcpkix-jdk15to18"
}In Application.onCreate, initialize the SDK:
@OptIn(DependencyAccessor::class)
public abstract class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// First, install Conscrypt and Bouncy Castle security providers.
// Disable patented algorithms in Bouncy Castle
System.setProperty("org.bouncycastle.ec.disable_mqv", "true")
try {
Security.removeProvider("BC")
} catch (e: Throwable) {
// Ignore if it's missing.
}
Security.addProvider(BouncyCastleProvider())
Security.insertProviderAt(Conscrypt.newProvider(), 1)
// Now init the SDK
sdkDeps = object : SdkDependencies() {
override val application: Application = this@MyApplication
}
}
}The architecture in this project is based on our experience with significantly larger projects at IBM. At a certain project size, you start to see which patterns break down and turn into bug sources and time wasters. So, expect to see some patterns that might look unusual at first sight.
Our project uses a library based architecture. In other words, the internal structure follows the same principle as when utilizing third-party dependencies. We explicitly don't break with the library abstraction within the app by introducing framework hollywood principles.
We avoid unnecessary indirections and abstraction layers as long as a simple combination of the IDE's refactoring operations can trivially introduce those layers later.
We use lifecycle-aware, reactive, demand-driven programming. See UI, reactivity, error handling, events for more details and sample code.
Important architectural concerns and pitfalls that have to be taken care of in the whole codebase are abstracted away instead of plastering the code with error-prone copy-paste logic. This includes error handling, correct lifecycle handling, and avoiding memory leaks - i.e. even trivial things like setting view bindings to null in onDestroyView. Everything is automated as much as possible behind simple APIs, so mistakes become less likely and complexity is kept low outside of these helpful abstractions/APIs. As long as you follow these APIs you're on the safe side, avoiding Android's pitfalls.
We are explicitly not using Dagger or Koin or any other framework for DI.
All of these tools have a non-trivial learning curve in comparison to what they're doing.
What is a DI framework doing, anyway?
In other words, Kotlin already provides everything you need for handling DI. So, we use pure, code-based DI.
In our much larger projects at IBM this solution has proven to work significantly better than Koin or Dagger.
In order to automatically deal with Android's architecture details and pitfalls we utilize ReactiveState/BaseReactiveState subclasses which can be used for (multiplatform) ViewModels or stateful UseCases. Internally, the instances live on an Android ViewModel (having the desired lifecycle), but they can be composed and tested more easily and in theory they allow reuse in multiplatform projects (though that's just a minor aspect in this app).
A ReactiveState comes with an eventNotifier to communicate events out-of-band (e.g. outside of the Fragment's lifecycle). Also, ReactiveState provides a launch method to launch coroutines with automatic error handling. Any errors are automatically forwarded to the UI via eventNotifier and trigger the Fragment's onError(error: Throwable) method. Typically you'd use MutableStateFlow (or the mutation-optimized MutableValueFlow) in order to provide observable values.
A ViewModel / ReactiveState implementation can be attached to a Fragment using by reactiveState which is lifecycle-aware.
Simple example:
// BaseEvents defines our always-available events. Currently this only contains
// onError(error: Throwable).
// We use interfaces instead of sealed classes to represent events because that is more
// composable (almost like union types) and results in less boilerplate.
interface MyEvents : BaseEvents {
fun onSomethingHappened(result: String)
}
// Usually the scope is passed from outside (in our case this will be the viewModelScope).
class MyViewModel(scope: CoroutineScope) : BaseReactiveState<MyEvents>(scope) {
val data = MutableStateFlow<List<Entity>>(emptyList())
fun refreshData() {
// This launches a coroutine, catches any exceptions and forwards them via
// eventNotifier { onError(error) }
// and activates the `loading` state (unless you pass withLoading = null).
launch {
// If an exception is thrown here it'll automatically get caught and trigger
// MyFragment.onError(exception)
data.value = requestLatestData()
}
}
// You can also compose states. The otherState.eventNotifier and otherState.loading
// will get merged into MyViewModel.
val otherState by childReactiveState { OtherReactiveState(scope) }
// A contrived event example to get the point across
fun doSomething() {
launch {
val result: String = someBackendCommunication()
// Tell UI the result of doSomething (let's pretend this must be a one-time
// executed event e.g. showing a dialog)
eventNotifier { onSomethingHappened(result) }
}
}
}
// The fragment has to implement the events interface.
class MyFragment : BaseFragment(), MyEvents {
// "by reactiveState" internally creates an Android ViewModel to hold the MyViewModel instance.
// MyViewModel's eventNotifier and loading are automatically processed in a
// lifecycle-aware way during the >= STARTED state.
// The events are triggered as method calls on this fragment - e.g. onError(throwable).
// Whenever `loading` changes, this triggers setLoading(isLoading: Boolean).
val viewModel by reactiveState { MyViewModel(scope) } // here, scope is an alias for viewModelScope
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Here we observe the data and update the UI whenever it changes by using autoRun.
// The autoRun block will re-execute whenever viewModel.data is changed.
// The get() call tells autoRun to get the viewModel.data.value and marks viewModel.data
// as a dependency of the autoRun block.
// This principle also works with multiple get() calls and can even be
// used together with if/when-branches to track some dependencies only under certain
// conditions (we even utilize this in our app - avoiding complicated Flow constructs).
// Moreover, autoRun is lifecycle-aware and only executes in the >= STARTED state.
autoRun {
updateUI(get(viewModel.data))
}
}
private fun updateUI(data: List<Entity>) {
// ...
}
override fun onSomethingHappened(result: String) {
// handle result event
}
}Note, the equivalent of
autoRun {
updateUI(get(viewModel.data))
}is more or less this block of code:
lifecycleScope.launchWhenStarted {
viewModel.data.collect {
try {
updateUI(it)
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) {
onError(e)
}
}
}This was just the trivial case with a single StateFlow. Imagine how complex things can become in the multi-StateFlow case combined with if/when and demand-driven resource allocation. We want to avoid this repetitive complexity/boilerplate and prevent common mistakes like forgetting to give CancellationException a special treat.
Use the by viewBinding helper which takes care of the whole lifecycle handling for you:
class DetailFragment : BaseFragment() {
private val binding by viewBinding(DetailBinding::inflate)
}With a single line of code, the binding is automatically inflated in onCreateView and cleared in onDestroyView, so you can avoid the whole boilerplate.
Use BaseFragmentStateAdapter which automatically avoids memory leaks. If that doesn't work for you, at least use Fragment.attachViewPager.
Use Fragment.attachToolbar to have automatically correct lifecycle handling and to avoid memory leaks.
Some of the modules in this repo were taken from our internal IBM projects. The code-based navigation system is one of them. Among the IBM developers who have worked with Android's Navigation component the experience was more on the negative side. Especially when creating SDKs and modularizing your code, the Navigation component can have its pitfalls, runtime crashes and it can get in your way. So, instead of using the Navigation components, we created a very simple code-based navigation system.
In this project we don't utilize the full flexibility of the navigation system. When we started out we simply wanted to build on the same infrastructure that we were already most familiar with and not risk regretting the decision to use the Navigation components. Moreover, this project is supposed to become a set of SDKs - one of them providing flexible, partial integration of the UI and navigation subgraphs.
This is how you define a navigation point and access the arguments:
@Parcelize
class DetailFragmentNav(val certId: String) : FragmentNav(DetailFragment::class)
class DetailFragment : BaseFragment() {
private val args: DetailFragmentNav by lazy { getArgs() }
// ...
// Within Fragments you can optionally customize the back button behavior
override fun onBackPressed(): Abortable {
if (something) {
customBackPressLogic()
return Abort // aborts the default behavior
}
return Continue // continue with default back behavior
}
}This is how you navigate:
findNavigator().push(DetailFragmentNav(args.certId))
findNavigator().pop()
findNavigator().popAll()
findNavigator().popUntil(SomeFragment::class)
// For passing results you define an interface and popUntil a fragment that
// implements the interface
interface DetailFragmentListener { fun onDetailResult(value: String) }
findNavigator().popUntil<DetailFragmentListener>()?.onDetailResult(someValue)
// triggers onBackPressed()
triggerBackPress()So, the API is similar to the Navigation component, but everything is code-based (no XML) and thus easier to reason about and you get more control.
Copyright (C) 2021 IBM Deutschland GmbH Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
| Back | FazBrowse Home | New Git URL |