Introduce MvRx in the application + start managing UI

This commit is contained in:
ganfra 2018-10-28 19:18:14 +01:00
parent d0a241bd2d
commit e5fc1e3412
52 changed files with 805 additions and 133 deletions

View File

@ -10,7 +10,7 @@
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/matrix-sdk-android" />
<option value="$PROJECT_DIR$/matrix-sdk-rx" />
<option value="$PROJECT_DIR$/matrix-sdk-android-rx" />
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />

View File

@ -41,6 +41,7 @@ dependencies {

implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(":matrix-sdk-android")
implementation project(":matrix-sdk-android-rx")

implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.android.support:appcompat-v7:28.0.0'
@ -51,11 +52,11 @@ dependencies {
implementation("com.airbnb.android:epoxy:$epoxy_version")
kapt "com.airbnb.android:epoxy-processor:$epoxy_version"
implementation "com.airbnb.android:epoxy-paging:$epoxy_version"
implementation 'com.airbnb.android:mvrx:0.6.0'


implementation "org.koin:koin-android:$koin_version"
implementation "org.koin:koin-android-scope:$koin_version"
implementation "org.koin:koin-android-viewmodel:$koin_version"

testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'

View File

@ -0,0 +1,45 @@
package im.vector.riotredesign.core.platform

import android.content.Context
import android.support.constraint.ConstraintLayout
import android.util.AttributeSet
import android.view.View
import android.widget.Checkable

class CheckableConstraintLayout : ConstraintLayout, Checkable {

private var mChecked = false

constructor(context: Context) : super(context) {}

constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {}

constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {}

override fun isChecked(): Boolean {
return mChecked
}

override fun setChecked(b: Boolean) {
if (b != mChecked) {
mChecked = b
refreshDrawableState()
}
}

override fun toggle() {
isChecked = !mChecked
}

public override fun onCreateDrawableState(extraSpace: Int): IntArray {
val drawableState = super.onCreateDrawableState(extraSpace + 1)
if (isChecked) {
View.mergeDrawableStates(drawableState, CHECKED_STATE_SET)
}
return drawableState
}

companion object {
private val CHECKED_STATE_SET = intArrayOf(android.R.attr.state_checked)
}
}

View File

@ -1,7 +1,5 @@
package im.vector.riotredesign.core.platform

import android.support.v7.app.AppCompatActivity
import com.airbnb.mvrx.BaseMvRxActivity

open class RiotActivity : AppCompatActivity() {

}
abstract class RiotActivity : BaseMvRxActivity()

View File

@ -1,6 +1,12 @@
package im.vector.riotredesign.core.platform

import android.support.v4.app.Fragment
import com.airbnb.mvrx.BaseMvRxFragment

abstract class RiotFragment : BaseMvRxFragment() {

override fun invalidate() {
//no-ops by default
}


open class RiotFragment : Fragment() {
}

View File

@ -0,0 +1,81 @@
package im.vector.riotredesign.core.platform

import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import im.vector.riotredesign.R
import kotlinx.android.synthetic.main.view_state.view.*

class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0)
: FrameLayout(context, attrs, defStyle) {

sealed class State {
object Content : State()
object Loading : State()
data class Empty(val message: CharSequence? = null) : State()
data class Error(val message: CharSequence? = null) : State()
}


private var eventCallback: EventCallback? = null

var contentView: View? = null

var state: State = State.Empty()
set(newState) {
if (newState != state) {
update(newState)
}
}

interface EventCallback {
fun onRetryClicked()
}

init {
View.inflate(context, R.layout.view_state, this)
layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
errorRetryView.setOnClickListener {
eventCallback?.onRetryClicked()
}
state = State.Content
}


private fun update(newState: State) {
when (newState) {
is StateView.State.Content -> {
progressBar.visibility = View.INVISIBLE
errorView.visibility = View.INVISIBLE
emptyView.visibility = View.INVISIBLE
contentView?.visibility = View.VISIBLE
}
is StateView.State.Loading -> {
progressBar.visibility = View.VISIBLE
errorView.visibility = View.INVISIBLE
emptyView.visibility = View.INVISIBLE
contentView?.visibility = View.INVISIBLE
}
is StateView.State.Empty -> {
progressBar.visibility = View.INVISIBLE
errorView.visibility = View.INVISIBLE
emptyView.visibility = View.VISIBLE
emptyMessageView.text = newState.message
if (contentView != null) {
contentView!!.visibility = View.INVISIBLE
}
}
is StateView.State.Error -> {
progressBar.visibility = View.INVISIBLE
errorView.visibility = View.VISIBLE
emptyView.visibility = View.INVISIBLE
errorMessageView.text = newState.message
if (contentView != null) {
contentView!!.visibility = View.INVISIBLE
}
}
}
}
}

View File

@ -3,23 +3,37 @@ package im.vector.riotredesign.features.home
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.Gravity
import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.replaceFragment
import im.vector.riotredesign.core.platform.RiotActivity
import im.vector.riotredesign.features.home.detail.LoadingRoomDetailFragment
import im.vector.riotredesign.features.home.detail.RoomDetailFragment
import im.vector.riotredesign.features.home.list.RoomListFragment
import kotlinx.android.synthetic.main.activity_home.*
import org.koin.standalone.StandAloneContext.loadKoinModules


class HomeActivity : RiotActivity() {
class HomeActivity : RiotActivity(), HomeNavigator {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)

loadKoinModules(listOf(HomeModule(this)))
if (savedInstanceState == null) {
val roomListFragment = RoomListFragment.newInstance()
replaceFragment(roomListFragment, R.id.homeFragmentContainer)
val loadingDetail = LoadingRoomDetailFragment.newInstance()
replaceFragment(loadingDetail, R.id.homeDetailFragmentContainer)
replaceFragment(roomListFragment, R.id.homeDrawerFragmentContainer)
}
}

override fun openRoomDetail(roomId: String) {
val roomDetailFragment = RoomDetailFragment.newInstance(roomId)
replaceFragment(roomDetailFragment, R.id.homeDetailFragmentContainer)
drawerLayout.closeDrawer(Gravity.LEFT)
}

companion object {
fun newIntent(context: Context): Intent {
return Intent(context, HomeActivity::class.java)

View File

@ -0,0 +1,16 @@
package im.vector.riotredesign.features.home

import org.koin.dsl.context.ModuleDefinition
import org.koin.dsl.module.Module
import org.koin.dsl.module.module

class HomeModule(private val homeActivity: HomeActivity) : Module {

override fun invoke(): ModuleDefinition = module(override = true) {

factory {
homeActivity as HomeNavigator
}

}.invoke()
}

View File

@ -0,0 +1,7 @@
package im.vector.riotredesign.features.home

interface HomeNavigator {

fun openRoomDetail(roomId: String)

}

View File

@ -1,51 +0,0 @@
package im.vector.riotredesign.features.home

import android.arch.lifecycle.Observer
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.addFragmentToBackstack
import im.vector.riotredesign.core.platform.RiotFragment
import kotlinx.android.synthetic.main.fragment_room_list.*
import org.koin.android.ext.android.inject

class RoomListFragment : RiotFragment(), RoomSummaryController.Callback {

companion object {

fun newInstance(): RoomListFragment {
return RoomListFragment()
}

}

private val matrix by inject<Matrix>()
private val currentSession = matrix.currentSession!!
private lateinit var roomController: RoomSummaryController

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_room_list, container, false)
}

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
roomController = RoomSummaryController(this)
epoxyRecyclerView.setController(roomController)
currentSession.liveRoomSummaries().observe(this, Observer<List<RoomSummary>> { renderRooms(it) })
}

private fun renderRooms(rooms: List<RoomSummary>?) {
roomController.setData(rooms)
}

override fun onRoomSelected(room: RoomSummary) {
val detailFragment = RoomDetailFragment.newInstance(room.roomId)
addFragmentToBackstack(detailFragment, R.id.homeFragmentContainer)
}


}

View File

@ -1,21 +0,0 @@
package im.vector.riotredesign.features.home

import com.airbnb.epoxy.TypedEpoxyController
import im.vector.matrix.android.api.session.room.model.RoomSummary

class RoomSummaryController(private val callback: Callback? = null
) : TypedEpoxyController<List<RoomSummary>>() {

override fun buildModels(data: List<RoomSummary>?) {
data?.forEach {
RoomItem(it.displayName, listener = { callback?.onRoomSelected(it) })
.id(it.roomId)
.addTo(this)
}
}

interface Callback {
fun onRoomSelected(room: RoomSummary)
}

}

View File

@ -1,4 +1,4 @@
package im.vector.riotredesign.features.home
package im.vector.riotredesign.features.home.detail

import android.support.v7.util.DiffUtil
import im.vector.matrix.android.api.session.events.model.EnrichedEvent

View File

@ -0,0 +1,24 @@
package im.vector.riotredesign.features.home.detail

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.RiotFragment

class LoadingRoomDetailFragment : RiotFragment() {

companion object {

fun newInstance(): LoadingRoomDetailFragment {
return LoadingRoomDetailFragment()
}
}

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_loading_room_detail, container, false)
}


}

View File

@ -1,4 +1,4 @@
package im.vector.riotredesign.features.home
package im.vector.riotredesign.features.home.detail

import android.arch.lifecycle.Observer
import android.arch.paging.PagedList

View File

@ -1,4 +1,4 @@
package im.vector.riotredesign.features.home
package im.vector.riotredesign.features.home.detail

import android.arch.paging.PagedList
import android.arch.paging.PagedListAdapter

View File

@ -1,9 +1,10 @@
package im.vector.riotredesign.features.home
package im.vector.riotredesign.features.home.detail

import com.airbnb.epoxy.EpoxyAsyncUtil
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.paging.PagedListEpoxyController
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.riotredesign.features.home.LoadingItemModel_

class TimelineEventController : PagedListEpoxyController<Event>(
diffingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()

View File

@ -1,4 +1,4 @@
package im.vector.riotredesign.features.home
package im.vector.riotredesign.features.home.detail

import android.widget.TextView
import im.vector.riotredesign.R

View File

@ -0,0 +1,10 @@
package im.vector.riotredesign.features.home.list

import im.vector.matrix.android.api.session.room.model.RoomSummary

sealed class RoomListActions {

data class SelectRoom(val roomSummary: RoomSummary) : RoomListActions()


}

View File

@ -0,0 +1,82 @@
package im.vector.riotredesign.features.home.list

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.airbnb.mvrx.*
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.RiotFragment
import im.vector.riotredesign.core.platform.StateView
import im.vector.riotredesign.features.home.HomeNavigator
import kotlinx.android.synthetic.main.fragment_room_list.*
import org.koin.android.ext.android.inject

class RoomListFragment : RiotFragment(), RoomSummaryController.Callback {

companion object {
fun newInstance(): RoomListFragment {
return RoomListFragment()
}
}

private val homeNavigator by inject<HomeNavigator>()
private val viewModel: RoomListViewModel by fragmentViewModel()
private lateinit var roomController: RoomSummaryController

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_room_list, container, false)
}

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
roomController = RoomSummaryController(this)
stateView.contentView = epoxyRecyclerView
epoxyRecyclerView.setController(roomController)
viewModel.subscribe { renderState(it) }
}

private fun renderState(state: RoomListViewState) {
when (state.roomSummaries) {
is Incomplete -> renderLoading()
is Success -> renderSuccess(state.roomSummaries(), state.selectedRoom)
is Fail -> renderFailure(state.roomSummaries.error)
}
if (state.showLastSelectedRoom && state.selectedRoom != null) {
homeNavigator.openRoomDetail(state.selectedRoom.roomId)
}
}

private fun renderSuccess(roomSummaries: List<RoomSummary>?, selectedRoom: RoomSummary?) {
if (roomSummaries.isNullOrEmpty()) {
stateView.state = StateView.State.Empty("Rejoignez une room pour commencer à utiliser l'application")
} else {
stateView.state = StateView.State.Content
}
roomController.setData(roomSummaries, selectedRoom)
}

private fun renderLoading() {
stateView.state = StateView.State.Loading
}

private fun renderFailure(error: Throwable) {
val message = when (error) {
is Failure.NetworkConnection -> "Pas de connexion internet"
else -> "Une erreur est survenue"
}
stateView.state = StateView.State.Error(message)
}

override fun onRoomSelected(room: RoomSummary) {
withState(viewModel) {
if (it.selectedRoom != room) {
viewModel.accept(RoomListActions.SelectRoom(room))
homeNavigator.openRoomDetail(room.roomId)
}
}
}

}

View File

@ -0,0 +1,54 @@
package im.vector.riotredesign.features.home.list

import android.support.v4.app.FragmentActivity
import com.airbnb.mvrx.BaseMvRxViewModel
import com.airbnb.mvrx.MvRxViewModelFactory
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.rx.rx
import org.koin.android.ext.android.get

class RoomListViewModel(initialState: RoomListViewState,
private val session: Session
) : BaseMvRxViewModel<RoomListViewState>(initialState) {

companion object : MvRxViewModelFactory<RoomListViewState> {

@JvmStatic
override fun create(activity: FragmentActivity, state: RoomListViewState): RoomListViewModel {
val matrix = activity.get<Matrix>()
val currentSession = matrix.currentSession!!
return RoomListViewModel(state, currentSession)
}
}

init {
observeRoomSummaries()
}

fun accept(action: RoomListActions) {
when (action) {
is RoomListActions.SelectRoom -> handleSelectRoom(action)
}
}

// PRIVATE METHODS *****************************************************************************

private fun handleSelectRoom(action: RoomListActions.SelectRoom) {
session.saveLastSelectedRoom(action.roomSummary)
setState { copy(selectedRoom = action.roomSummary) }
}

private fun observeRoomSummaries() {
session
.rx().liveRoomSummaries()
.execute {
val selectedRoom = selectedRoom
?: session.lastSelectedRoom()
?: it.invoke()?.firstOrNull()

copy(roomSummaries = it, selectedRoom = selectedRoom)
}
}

}

View File

@ -0,0 +1,24 @@
package im.vector.riotredesign.features.home.list

import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.room.model.RoomSummary

data class RoomListViewState(
val roomSummaries: Async<List<RoomSummary>> = Uninitialized,
val selectedRoom: RoomSummary? = null
) : MvRxState {

var showLastSelectedRoom: Boolean = true
private set
get() {
if (field) {
field = false
return true
}
return false
}


}

View File

@ -0,0 +1,26 @@
package im.vector.riotredesign.features.home.list

import com.airbnb.epoxy.Typed2EpoxyController
import im.vector.matrix.android.api.session.room.model.RoomSummary

class RoomSummaryController(private val callback: Callback? = null

) : Typed2EpoxyController<List<RoomSummary>, RoomSummary>() {

override fun buildModels(summaries: List<RoomSummary>?, selected: RoomSummary?) {
summaries?.forEach {
RoomSummaryItem(
it.displayName,
isSelected = it == selected,
listener = { callback?.onRoomSelected(it) }
)
.id(it.roomId)
.addTo(this)
}
}

interface Callback {
fun onRoomSelected(room: RoomSummary)
}

}

View File

@ -1,17 +1,22 @@
package im.vector.riotredesign.features.home
package im.vector.riotredesign.features.home.list

import android.support.v4.content.ContextCompat
import android.widget.TextView
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.KotlinModel
import im.vector.riotredesign.core.platform.CheckableConstraintLayout

data class RoomItem(
data class RoomSummaryItem(
val title: CharSequence,
val isSelected: Boolean,
val listener: (() -> Unit)? = null
) : KotlinModel(R.layout.item_room) {

val titleView by bind<TextView>(R.id.titleView)
val rootView by bind<CheckableConstraintLayout>(R.id.itemRoomLayout)

override fun bind() {
rootView.isChecked = isSelected
titleView.setOnClickListener { listener?.invoke() }
titleView.text = title
}

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape>
<solid android:color="@android:color/white"/>
</shape>
</item>
<item android:state_checked="true">
<shape>
<solid android:color="@android:color/white"/>
</shape>
</item>
<item>
<shape>
<solid android:color="@android:color/transparent"/>
</shape>
</item>

</selector>

View File

@ -1,15 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawerLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".features.login.LoginActivity">
tools:openDrawer="start">

<FrameLayout
android:id="@+id/homeFragmentContainer"
android:id="@+id/homeDetailFragmentContainer"
android:layout_width="match_parent"
android:layout_height="match_parent" />

<FrameLayout
android:id="@+id/homeDrawerFragmentContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="start"
android:layout_marginRight="24dp" />

</android.support.constraint.ConstraintLayout>
</android.support.v4.widget.DrawerLayout>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">

<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

View File

@ -1,11 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<im.vector.riotredesign.core.platform.StateView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/stateView"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:background="@color/pale_grey">

<com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/epoxyRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />

</FrameLayout>
</im.vector.riotredesign.core.platform.StateView>

View File

@ -1,10 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"

<im.vector.riotredesign.core.platform.CheckableConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/itemRoomLayout"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/titleView"
android:layout_width="match_parent"
android:layout_height="80dp"
android:gravity="center_vertical"
android:padding="16dp"
android:textSize="14sp"
tools:text="Room name" />
android:layout_height="wrap_content"
android:background="@drawable/bg_room_item"
android:minHeight="80dp">

<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/titleView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Room name" />

</im.vector.riotredesign.core.platform.CheckableConstraintLayout>

View File

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="40dp"
android:padding="8dp"
tools:parentTag="android.widget.FrameLayout">

<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" />


<LinearLayout
android:id="@+id/errorView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">

<TextView
android:id="@+id/errorMessageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:textColor="@android:color/black"
android:textSize="16sp"
tools:text="Une erreur est survenue" />


<Button
android:id="@+id/errorRetryView"
android:layout_width="190dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="8dp"
android:text="@string/global_retry"
android:textColor="@android:color/white" />

</LinearLayout>


<LinearLayout
android:id="@+id/emptyView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">

<TextView
android:id="@+id/emptyMessageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:textColor="@android:color/black"
android:textSize="16sp" />


<ImageView
android:id="@+id/emptyImageView"
android:layout_width="190dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="8dp" />

</LinearLayout>

</merge>

View File

@ -3,4 +3,16 @@
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>


<color name="pale_grey">#f2f5f8</color>
<color name="dark">#2e3649</color>
<color name="pale_teal">#7ac9a1</color>
<color name="black">#212121</color>
<color name="deep_sky_blue">#007aff</color>
<color name="rosy_pink">#f56679</color>
<color name="bluey_grey">#a5a5a6</color>
<color name="slate_grey">#5f6268</color>
<color name="sky_blue">#7bb2ea</color>

</resources>

View File

@ -1,3 +1,6 @@
<resources>
<string name="app_name">Riot Redesign</string>

<string name="global_retry">Réessayer</string>

</resources>

View File

@ -0,0 +1,40 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'

android {
compileSdkVersion 28



defaultConfig {
minSdkVersion 16
targetSdkVersion 28
versionCode 1
versionName "1.0"

testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}

}

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(":matrix-sdk-android")
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'


testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,26 @@
package im.vector.matrix.rx;

import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;

import org.junit.Test;
import org.junit.runner.RunWith;

import static org.junit.Assert.*;

/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getTargetContext();

assertEquals("im.vector.matrix.rx.test", appContext.getPackageName());
}
}

View File

@ -0,0 +1,2 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="im.vector.matrix.rx" />

View File

@ -0,0 +1,45 @@
package im.vector.matrix.rx

import android.arch.lifecycle.LiveData
import android.arch.lifecycle.Observer
import io.reactivex.Observable
import io.reactivex.android.MainThreadDisposable

private class LiveDataObservable<T>(
private val liveData: LiveData<T>,
private val valueIfNull: T? = null
) : Observable<T>() {

override fun subscribeActual(observer: io.reactivex.Observer<in T>) {
val relay = RemoveObserverInMainThread(observer)
observer.onSubscribe(relay)
liveData.observeForever(relay)
}

private inner class RemoveObserverInMainThread(private val observer: io.reactivex.Observer<in T>)
: MainThreadDisposable(), Observer<T> {

override fun onChanged(t: T?) {
if (!isDisposed) {
if (t == null) {
if (valueIfNull != null) {
observer.onNext(valueIfNull)
} else {
observer.onError(NullPointerException(
"convert liveData value t to RxJava onNext(t), t cannot be null"))
}
} else {
observer.onNext(t)
}
}
}

override fun onDispose() {
liveData.removeObserver(this)
}
}
}

fun <T> LiveData<T>.asObservable(): Observable<T> {
return LiveDataObservable(this)
}

View File

@ -0,0 +1,17 @@
package im.vector.matrix.rx

import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.model.RoomSummary
import io.reactivex.Observable

class RxSession(private val session: Session) {

fun liveRoomSummaries(): Observable<List<RoomSummary>> {
return session.liveRoomSummaries().asObservable()
}

}

fun Session.rx(): RxSession {
return RxSession(this)
}

View File

@ -0,0 +1,3 @@
<resources>
<string name="app_name">matrix-sdk-android-rx</string>
</resources>

View File

@ -0,0 +1,17 @@
package im.vector.matrix.rx;

import org.junit.Test;

import static org.junit.Assert.*;

/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
}

View File

@ -2,22 +2,12 @@ package im.vector.matrix.android.api.failure

import java.io.IOException

sealed class Failure {
sealed class Failure(cause: Throwable? = null) : Throwable(cause = cause) {

data class Unknown(val exception: Exception? = null) : Failure()
data class NetworkConnection(val ioException: IOException) : Failure()
data class ServerError(val error: MatrixError) : Failure()
data class Unknown(val throwable: Throwable? = null) : Failure(throwable)
data class NetworkConnection(val ioException: IOException) : Failure(ioException)
data class ServerError(val error: MatrixError) : Failure(RuntimeException(error.toString()))

abstract class FeatureFailure : Failure()

fun toException(): Exception {
return when (this) {
is Unknown -> this.exception ?: RuntimeException("Unknown error")
is NetworkConnection -> this.ioException
is ServerError -> RuntimeException(this.error.toString())
is FeatureFailure -> RuntimeException("Feature error")
}

}

}

View File

@ -13,5 +13,9 @@ interface RoomService {

fun liveRoomSummaries(): LiveData<List<RoomSummary>>

fun lastSelectedRoom(): RoomSummary?

fun saveLastSelectedRoom(roomSummary: RoomSummary)


}

View File

@ -13,8 +13,20 @@ object RoomSummaryMapper {
roomSummaryEntity.topic ?: ""
)
}

internal fun map(roomSummary: RoomSummary): RoomSummaryEntity {
return RoomSummaryEntity(
roomSummary.roomId,
roomSummary.displayName,
roomSummary.topic
)
}
}

fun RoomSummaryEntity.asDomain(): RoomSummary {
return RoomSummaryMapper.map(this)
}

fun RoomSummaryEntity.asEntity(): RoomSummary {
return RoomSummaryMapper.map(this)
}

View File

@ -11,7 +11,8 @@ open class RoomSummaryEntity(@PrimaryKey var roomId: String = "",
var lastMessage: EventEntity? = null,
var heroes: RealmList<String> = RealmList(),
var joinedMembersCount: Int? = 0,
var invitedMembersCount: Int? = 0
var invitedMembersCount: Int? = 0,
var isLatestSelected: Boolean = false
) : RealmObject() {

companion object

View File

@ -13,3 +13,9 @@ fun RoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = null): Rea
}
return query
}

fun RoomSummaryEntity.Companion.lastSelected(realm: Realm): RoomSummaryEntity? {
return realm.where<RoomSummaryEntity>()
.equalTo(RoomSummaryEntityFields.IS_LATEST_SELECTED, true)
.findFirst()
}

View File

@ -75,6 +75,14 @@ class DefaultSession(override val sessionParams: SessionParams) : Session, KoinC
return roomService.liveRoomSummaries()
}

override fun lastSelectedRoom(): RoomSummary? {
return roomService.lastSelectedRoom()
}

override fun saveLastSelectedRoom(roomSummary: RoomSummary) {
roomService.saveLastSelectedRoom(roomSummary)
}

// Private methods *****************************************************************************

private fun checkIsMainThread() {

View File

@ -8,6 +8,7 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.query.lastSelected
import im.vector.matrix.android.internal.database.query.where

class DefaultRoomService(private val monarchy: Monarchy) : RoomService {
@ -42,5 +43,20 @@ class DefaultRoomService(private val monarchy: Monarchy) : RoomService {
)
}

override fun lastSelectedRoom(): RoomSummary? {
var lastSelected: RoomSummary? = null
monarchy.doWithRealm { realm ->
lastSelected = RoomSummaryEntity.lastSelected(realm)?.asDomain()
}
return lastSelected
}

override fun saveLastSelectedRoom(roomSummary: RoomSummary) {
monarchy.writeAsync { realm ->
val lastSelected = RoomSummaryEntity.lastSelected(realm)
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomSummary.roomId).findFirst()
lastSelected?.isLatestSelected = false
roomSummaryEntity?.isLatestSelected = true
}
}
}

View File

@ -53,7 +53,7 @@ class TimelineBoundaryCallback(private val roomId: String,
}

override fun onFailure(failure: Failure) {
pagingRequestCallback.recordFailure(failure.toException())
pagingRequestCallback.recordFailure(failure)
}
}
}

View File

@ -1,8 +0,0 @@
apply plugin: 'java-library'
apply plugin: "kotlin"

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'

}

View File

@ -1,3 +0,0 @@
package im.vector.matrix.rx

class MatrixRx

View File

@ -1 +1 @@
include ':app', ':matrix-sdk-rx', ':matrix-sdk-android'
include ':app', ':matrix-sdk-android', ':matrix-sdk-android-rx'