Awesome
DroidKaigi 2020 official Android app
The DroidKaigi 2020 event has been canceled and development is inactive, but we'd love to hear from you if you have any improvements.
You can install the production app via Get it on Google Play.
And also, you can try the binary under development built on master branch through Try it on your device via DeployGate
<img src="https://dply.me/qo28h3/button/large" alt="Try it on your device via DeployGate">
Features
top | drawer | |
---|---|---|
<img src="arts/top.png" width="250px" /> | <img src="arts/drawer.png" width="250px" /> |
Contributing
We always welcome any and all contributions! See CONTRIBUTING.md for more information
For Japanese speakers, please see CONTRIBUTING.ja.md
Requirements
Android Studio 3.6 and higher. You can download it from this page.
Development Environment
Multi module project
We separate the modules for each feature. We use the Dynamic feature modules for additional features.
<img src="arts/multimodule.png" width="400px" />Kotlin Multiplatform Project
This app uses Kotlin Multiplatoform to share API and model classes (such as Session and Room classes) between Android and iOS.
Architecture
This app uses an AndroidJetpack(AAC) based architecture using AAC(LiveData, ViewModel, Room), Kotlin, Kotlin Coroutines Flow, DataBinding, Dagger, Firebase.
<img src="arts/arch1.png" width="400px" />It is designed to be a unidirectional data flow within the ViewModel.
<img src="arts/arch2.png" width="400px" />Fragment
<img src="arts/fragment.png" width="400px" />Just observe() the LiveData<UiModel>
of the ViewModel.
@Inject lateinit var sessionDetailViewModelFactory: SessionDetailViewModel.Factory
private val sessionDetailViewModel by assistedViewModels {
sessionDetailViewModelFactory.create(navArgs.sessionId)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
...
sessionDetailViewModel.uiModel
.observe(viewLifecycleOwner) { uiModel: SessionDetailViewModel.UiModel ->
...
progressTimeLatch.loading = uiModel.isLoading
uiModel.session
?.let { session -> setupSessionViews(session) }
}
}
ViewModel
<img src="arts/viewmodel1.png" width="400px" />The LiveData Kotlin Coroutines builder runs when LiveData becomes active.
And observe the data of the Coroutiens Flow of the repository.
The LiveData becomes LoadState.Loading before the Coroutiens Flow is executed by Flow.toLoadingState(), and becomes LoadState.Loaded when finished.
class SessionsViewModel @Inject constructor(
val sessionRepository: SessionRepository
) : ViewModel() {
...
private val sessionLoadState: LiveData<LoadState<SessionContents>> = liveData {
emitSource(
sessionRepository.sessionContents()
.toLoadingState()
.asLiveData()
)
sessionRepository.refresh()
}
<img src="arts/viewmodel2.png" width="400px" />
Construct UiModel LiveData from some such LiveData.
The combine
method works like RxJava's combineLatest.
You can make the loading state of the screen from multiple LiveData states like sessionLoadState.isLoading || favoriteState.isLoading
.
class SessionDetailViewModel @AssistedInject constructor(
@Assisted private val sessionId: SessionId,
private val sessionRepository: SessionRepository
) : ViewModel() {
...
val uiModel: LiveData<UiModel> = combine(
initialValue = UiModel.EMPTY,
liveData1 = sessionLoadStateLiveData,
liveData2 = favoriteLoadingStateLiveData
) { current: UiModel,
sessionLoadState: LoadState<Session>,
favoriteState: LoadingState ->
// You can create loading state by multiple LiveData
val isLoading = sessionLoadState.isLoading || favoriteState.isLoading
UiModel(
isLoading = isLoading,
error = sessionLoadState
.getErrorIfExists()
.toAppError()
?: favoriteState
.getErrorIfExists()
.toAppError()
,
session = sessionLoadState.value
)
}
Run Coroutines with viewModelScope
when data changes, such as adding a session to Favorites.
Because we do not want to end the process of adding a session to favorites with the back button, we use WorkManager to do the processing.
class SessionDetailViewModel @AssistedInject constructor(
@Assisted private val sessionId: SessionId,
private val sessionRepository: SessionRepository
) : ViewModel() {
..
private var favoriteLoadingStateLiveData: MutableLiveData<LoadingState> = MutableLiveData(LoadingState.Loaded)
...
fun favorite(session: Session) {
viewModelScope.launch {
favoriteLoadingStateLiveData.value = LoadingState.Loading
try {
sessionRepository.toggleFavoriteWithWorker(session.id)
favoriteLoadingStateLiveData.value = LoadingState.Loaded
} catch (e: Exception) {
favoriteLoadingStateLiveData.value = LoadingState.Error(e)
}
}
}
Design
- DroidKaigi 2020 Design Kit
https://www.figma.com/file/RPPQQRys8IubNShKan8c2Z/DroidKaigi-2020-Design-Kit?node-id=0%3A2347 - App
https://www.figma.com/file/4r9becvhDy3GfXaXex8E8d/App
Thanks
Thank you for contributing!
- Contributors
- Designer
Credit
This project uses some modern Android libraries and source codes.
Android
- Android Jetpack (Google)
- Foundation
- AppCompat
- Android KTX
- Multidex
- Test
- Architecture
- Data Binding
- Lifecycles
- LiveData
- Navigation
- UI
- Emoji
- Fragment
- Transition
- ConstraintLayout
- RecyclerView
- ...
- Foundation
- Kotlin (JetBrains)
- Stdlib
- Coroutines
- Coroutines Flow
- Serialization
- Firebase (Google)
- Authentication
- Cloud Firestore
- Dagger 2
- Core (Google)
- AndroidSupport (Google)
- AssistedInject (Square)
- Material Components for Android (Google)
- Ktor (JetBrains)
- Android Client
- Json
- OkHttp (Square)
- Client
- LoggingInterceptor
- Coil (Coil Contributors)
- LeakCanary (Square)
- Stetho (Facebook)
- Hyperion-Android (WillowTree)
- Groupie (lisawray)
- KLOCK (soywiz)
- MockK (oleksiyp)
- Injected ViewModel Provider (evant)
- Google I/O 2018 (Google)
- TimetableLayout (MoyuruAizawa)
- Android Architecture Components samples (Google)
- Android Animation Samples Repository (Google)
- Store4 (Dropbox)