DroidKaigi 2020 official Android app

We are currently working on the event. We are looking for contributors!

DroidKaigi 2020 is a conference tailored for developers on 20th and 21st February 2020.

You can install the prodution app via Get it on Google Play.
// TODO: Add link to Google Play

And also, you can try the binary under development built on master branch through Try it on your device via DeployGate

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.

multimodule

Kotlin Multiplatform Project

// TODO: Add MultiPlatform

Architecture

This app uses an AndroidJetpack(AAC) based architecture using AAC(LiveData, ViewModel, Room), Kotlin, Kotlin Coroutines Flow, DataBinding, Dagger, Firebase.

arch1-1

It is designed to be a unidirectional data flow within the ViewModel.

arch2

Fragment

fragment

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

viewmodel1

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()
    }

viewmodel2

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.

viewmodel3

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

design

GitHub