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:layout_width="match_parent"
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="80dp"
android:gravity="center_vertical"
android:padding="16dp"
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'