Introduce mavericks-compose and room list module - WIP

This commit is contained in:
Benoit Marty
2022-10-11 16:18:42 +02:00
parent 98eeb08772
commit 8ce670995a
18 changed files with 328 additions and 43 deletions

View File

@@ -57,6 +57,7 @@ android {
dependencies {
implementation project(":libraries:ui:theme")
implementation project(":libraries:ui:screens:login")
implementation project(":libraries:ui:screens:roomlist")
implementation project(":libraries:sdk:matrix")
implementation 'androidx.core:core-ktx:1.9.0'
@@ -67,4 +68,6 @@ dependencies {
implementation 'androidx.activity:activity-compose:1.6.0'
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
implementation 'com.airbnb.android:mavericks-compose:2.7.0'
}

View File

@@ -1,6 +1,7 @@
package io.element.android.x
import android.app.Application
import com.airbnb.mvrx.Mavericks
import io.element.android.x.sdk.matrix.MatrixInstance
class ElementXApplication : Application() {
@@ -8,5 +9,6 @@ class ElementXApplication : Application() {
override fun onCreate() {
super.onCreate()
MatrixInstance.init(this)
Mavericks.initialize(this)
}
}

View File

@@ -3,14 +3,30 @@ package io.element.android.x
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import io.element.android.x.ui.screen.login.LoginActivity
import io.element.android.x.ui.screen.login.RoomListActivity
class MainActivity : ComponentActivity() {
private val launcher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
// Launch the room Activity and finish
startRoomActivityAndFinish()
} else {
finish()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Just start the LoginActivity for now.
// TODO if a session exist, start the room list
startActivity(Intent(this, LoginActivity::class.java))
launcher.launch(Intent(this, LoginActivity::class.java))
}
private fun startRoomActivityAndFinish() {
startActivity(Intent(this, RoomListActivity::class.java))
finish()
}
}

View File

@@ -0,0 +1,51 @@
plugins {
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
}
android {
namespace 'io.element.android.x.core'
compileSdk 33
defaultConfig {
minSdk 29
targetSdk 33
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion compose_version
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.appcompat:appcompat:1.5.1'
implementation 'com.google.android.material:material:1.6.1'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1"
implementation 'androidx.activity:activity-compose:1.6.0'
implementation 'androidx.fragment:fragment-ktx:1.5.3'
implementation 'com.airbnb.android:mavericks-compose:2.7.0'
}

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@@ -36,6 +36,7 @@ android {
}
dependencies {
implementation project(":libraries:core")
implementation project(":libraries:ui:theme")
implementation project(":libraries:sdk:matrix")
@@ -55,4 +56,6 @@ dependencies {
implementation 'androidx.activity:activity-compose:1.6.0'
implementation 'androidx.fragment:fragment-ktx:1.5.3'
implementation 'com.airbnb.android:mavericks-compose:2.7.0'
}

View File

@@ -1,25 +1,31 @@
package io.element.android.x.ui.screen.login
import android.app.Activity
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.collectAsState
import androidx.compose.material3.Text
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.compose.collectAsState
import com.airbnb.mvrx.compose.mavericksViewModel
import io.element.android.x.ui.theme.ElementXTheme
import io.element.android.x.ui.theme.components.VectorButton
import io.element.android.x.ui.theme.components.VectorTextField
class LoginActivity : ComponentActivity() {
private val viewModel: LoginViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -35,9 +41,10 @@ class LoginActivity : ComponentActivity() {
Column(
modifier = Modifier.fillMaxSize()
) {
val state = viewModel.state.collectAsState().value
VectorTextField(
value = state.homeserver,
val viewModel: LoginViewModel = mavericksViewModel()
val state by viewModel.collectAsState()
val isError = state.isLoggedIn is Fail
VectorTextField(value = state.homeserver,
onValueChange = {
viewModel.handle(LoginActions.SetHomeserver(it))
})
@@ -50,18 +57,42 @@ class LoginActivity : ComponentActivity() {
value = state.password,
onValueChange = {
viewModel.handle(LoginActions.SetPassword(it))
}
},
isError = isError
)
if (isError) {
Text(
text = (state.isLoggedIn as? Fail)?.toString().orEmpty(),
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(start = 16.dp)
)
}
VectorButton(
text = "Submit",
onClick = {
viewModel.handle(LoginActions.Submit)
},
enabled = state.submitEnabled,
modifier = Modifier.align(Alignment.End)
)
if (state.isLoggedIn is Loading) {
// FIXME This does not work, we never enter this if block
CircularProgressIndicator(
modifier = Modifier.align(Alignment.CenterHorizontally)
)
}
if (state.isLoggedIn is Success) {
openRoomList()
}
}
}
}
}
}
private fun openRoomList() {
setResult(Activity.RESULT_OK)
finish()
}
}

View File

@@ -1,26 +1,38 @@
package io.element.android.x.ui.screen.login
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MavericksViewModel
import com.airbnb.mvrx.Success
import io.element.android.x.sdk.matrix.MatrixInstance
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
class LoginViewModel : ViewModel() {
class LoginViewModel(initialState: LoginViewState) :
MavericksViewModel<LoginViewState>(initialState) {
private val matrix = MatrixInstance.getInstance()
private val _state = MutableStateFlow(LoginViewState())
val state = _state.asStateFlow()
init {
observeState()
}
private fun observeState() {
// TODO Update submitEnabled when other state members are updated.
onEach(
LoginViewState::homeserver,
LoginViewState::login,
LoginViewState::password,
LoginViewState::isLoggedIn,
) { homeserver, login, password, isLoggedIn ->
setState {
copy(
submitEnabled = homeserver.isNotEmpty() &&
login.isNotEmpty() &&
password.isNotEmpty() &&
isLoggedIn !is Loading
)
}
}
}
fun handle(action: LoginActions) {
@@ -33,40 +45,40 @@ class LoginViewModel : ViewModel() {
}
private fun handleSetHomeserver(action: LoginActions.SetHomeserver) {
_state.value = _state.value.copy(
homeserver = action.homeserver,
submitEnabled = _state.value.login.isNotEmpty() &&
_state.value.password.isNotEmpty() &&
action.homeserver.isNotEmpty()
)
setState {
copy(
homeserver = action.homeserver
)
}
}
private fun handleSubmit() {
private fun handleSubmit() = withState { state ->
viewModelScope.launch {
val currentState = state.value
setState { copy(isLoggedIn = Loading()) }
try {
matrix.login(currentState.homeserver, currentState.login, currentState.password)
matrix.login(state.homeserver, state.login, state.password)
setState { copy(isLoggedIn = Success(Unit)) }
} catch (throwable: Throwable) {
Log.e("Error", "Cannot login", throwable)
setState { copy(isLoggedIn = Fail(throwable)) }
}
}
}
private fun handleSetPassword(action: LoginActions.SetPassword) {
_state.value = _state.value.copy(
password = action.password,
submitEnabled = _state.value.login.isNotEmpty() &&
_state.value.homeserver.isNotEmpty() &&
action.password.isNotEmpty()
)
setState {
copy(
password = action.password
)
}
}
private fun handleSetName(action: LoginActions.SetLogin) {
_state.value = _state.value.copy(
login = action.login,
submitEnabled = action.login.isNotEmpty() &&
_state.value.homeserver.isNotEmpty() &&
_state.value.password.isNotEmpty()
)
setState {
copy(
login = action.login
)
}
}
}

View File

@@ -1,8 +1,13 @@
package io.element.android.x.ui.screen.login
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.Uninitialized
data class LoginViewState(
val homeserver: String = "matrix.org",
val login: String = "",
val password: String = "",
val submitEnabled: Boolean = false,
)
val isLoggedIn: Async<Unit> = Uninitialized,
) : MavericksState

View File

@@ -0,0 +1,61 @@
plugins {
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
}
android {
namespace 'io.element.android.x.ui.screen.roomlist'
compileSdk 33
defaultConfig {
minSdk 29
targetSdk 33
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion compose_version
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation project(":libraries:core")
implementation project(":libraries:ui:theme")
implementation project(":libraries:sdk:matrix")
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.appcompat:appcompat:1.5.1'
implementation 'com.google.android.material:material:1.6.1'
implementation "androidx.compose.ui:ui:$compose_version"
implementation 'androidx.compose.material3:material3:1.0.0-rc01'
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1"
implementation 'androidx.activity:activity-compose:1.6.0'
implementation 'androidx.fragment:fragment-ktx:1.5.3'
implementation 'com.airbnb.android:mavericks-compose:2.7.0'
}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity android:name="RoomListActivity" />
</application>
</manifest>

View File

@@ -0,0 +1,5 @@
package io.element.android.x.ui.screen.login
sealed interface RoomListActions {
object LoadMore : RoomListActions
}

View File

@@ -0,0 +1,47 @@
package io.element.android.x.ui.screen.login
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import io.element.android.x.ui.theme.ElementXTheme
class RoomListActivity : ComponentActivity() {
private val viewModel: RoomListViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ElementXTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
color = MaterialTheme.colorScheme.background
) {
Column(
modifier = Modifier.fillMaxSize()
) {
/* TODO
val state = viewModel.state.collectAsState().value
RoomListHeader()
RoomList()
*/
}
}
}
}
}
}

View File

@@ -0,0 +1,28 @@
package io.element.android.x.ui.screen.login
import androidx.lifecycle.ViewModel
import io.element.android.x.sdk.matrix.MatrixInstance
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
class RoomListViewModel : ViewModel() {
private val matrix = MatrixInstance.getInstance()
private val _state = MutableStateFlow(RoomListViewState())
val state = _state.asStateFlow()
init {
observeState()
}
private fun observeState() {
// TODO Update submitEnabled when other state members are updated.
}
fun handle(action: RoomListActions) {
when (action) {
RoomListActions.LoadMore -> TODO()
}
}
}

View File

@@ -0,0 +1,6 @@
package io.element.android.x.ui.screen.login
data class RoomListViewState(
val list: List<String> = emptyList(),
val canLoadMore: Boolean = false,
)

View File

@@ -3,13 +3,15 @@ package io.element.android.x.ui.theme.components
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@Composable
fun VectorButton(text: String, enabled: Boolean, onClick: () -> Unit) {
fun VectorButton(text: String, enabled: Boolean, onClick: () -> Unit, modifier: Modifier? = null) {
Button(
onClick = onClick,
enabled = enabled,
modifier = modifier ?: Modifier
) {
Text(text = text)
}

View File

@@ -9,10 +9,11 @@ import androidx.compose.ui.Modifier
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun VectorTextField(value: String, onValueChange: (String) -> Unit) {
fun VectorTextField(value: String, onValueChange: (String) -> Unit, isError: Boolean = false) {
OutlinedTextField(
value = value,
onValueChange = onValueChange,
modifier = Modifier.fillMaxWidth()
modifier = Modifier.fillMaxWidth(),
isError = isError
)
}

View File

@@ -17,6 +17,8 @@ dependencyResolutionManagement {
}
rootProject.name = "ElementX"
include ':app'
include ':libraries:core'
include ':libraries:ui:theme'
include ':libraries:ui:screens:login'
include ':libraries:ui:screens:roomlist'
include ':libraries:sdk:matrix'