I'm trying to achieve a hide/show functionality for status bar in an AbstractComposeView
class MyView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AbstractComposeView(context, attrs, defStyleAttr) {
private val hide = mutableStateOf(false)
var hideBars: Boolean
get() = hide.value
set(value) { hide.value = value }
#Composable
override fun Content() {
val systemUiController = rememberSystemUiController()
SideEffect {
systemUiController.isStatusBarVisible = !hide.value
}
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnDetachedFromWindow)
Launch()
}
}
When setting hideBars = true/false it's not having any effect
view.findViewById<MyView>(R.id.my_view).hideBars = true
Any suggestions or workarounds?
SideEffect is just that - a side effect of something else causing a recomposition - changes to values read inside SideEffect do not cause recompositions themselves.
Instead, you should consider using LaunchedEffect, which runs every time the key passed to it changes:
LaunchedEffect(hide.value) {
systemUiController.isStatusBarVisible = !hide.value
}
Related
Am new to Android Development in general and especially with Jetpack Compose and its ways of updating the Composables. I have a iOS background with lots of SwiftUI though.
Anyways, I have the following app
The Composables look like this:
#Composable
fun Greeting() {
Column(
modifier = Modifier
.fillMaxHeight()2
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceEvenly
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
IncrementButton()
DecrementButton()
}
PressedText()
SizeText()
}
}
#Composable
fun PressedText() {
val myViewModel: MyViewModel = viewModel()
val myNumber by myViewModel.number.observeAsState()
Text(text = "Pressed: $myNumber")
}
#Composable
fun SizeText() {
val myViewModel: MyViewModel = viewModel()
val myList by myViewModel.list.observeAsState()
Text(text = "Size: ${myList?.size}")
}
#Composable
fun IncrementButton() {
val myViewModel: MyViewModel = viewModel()
Button(onClick = myViewModel::add) {
Text("Add")
}
}
#Composable
fun DecrementButton() {
val myViewModel: MyViewModel = viewModel()
Button(onClick = myViewModel::remove) {
Text("Remove")
}
}
The view model I am using looks like this:
class MyViewModel : ViewModel() {
private val _number = MutableLiveData<Int>()
val number: LiveData<Int> = _number
private val _list = MutableLiveData<MutableList<Int>>()
val list: LiveData<MutableList<Int>> = _list
init {
_number.value = 0
_list.value = mutableListOf()
}
fun add() {
_number.value = _number.value?.plus(1)
_number.value?.let {
_list.value?.add(it)
_list.value = _list.value
}
}
fun remove() {
_number.value = _number.value?.minus(1)
if (_list.value?.isNotEmpty() == true) {
_list.value?.removeAt(0)
_list.value = _list.value
}
}
}
When I press the "Add"-button the number after "Pressed" gets updated but not the number after "Size".
Am really not sure about those lines with _list.value = _list.value that I have from some other SO post that said to update the reference of the list.
What am I missing? Any hints highly appreciated.
Feel free to leave any comments regarding code design.
Thank you!
This _list.value = _list.value is a really bad idea. Depending on underlying implementation, it may work or may not. In this case it's probably compared by pointer, that's why it doesn't trigger recomposition.
Check out Why is immutability important in functional programming.
The safe way is using non mutable list:
private val _list = MutableLiveData<List<Int>>()
And mutate it like this:
_list.value = _list.value?.toMutableList()?.apply {
add(value)
}
By doing this, you're creating a new list each time, and this will trigger recomposition without problems.
Also, using LiveData is not required at all: if you don't have some dependencies, which makes you using it, you can go for Compose mutable state: it's much cleaner:
var number by mutableStateOf(0)
private set
private val _list = mutableStateListOf<Int>()
val list: List<Int> = _list
fun add() {
number++
_list.add(number)
}
fun remove() {
number--
_list.removeAt(0)
}
I'm working on floating window on android and i'm having a problem.
So I have my activity that calls my floating class that creates a the view and adds it to the windowmanager with a timer that is displayed and counting down. When trying the same code in a normal activity it works find :S?
Here is my viewmodel holding my mutableStateOf value
class WicViewModel : ViewModel() {
private val TAG = "WicViewModel"
private val a = 10000L
private val b = 1000L
private var _input: Long by mutableStateOf(a)
var input: Long
get() = _input
set(value) {
if (value == _input) return
_input = value
}
fun start() {
val timer = object : CountDownTimer(a, b) {
override fun onTick(millisUntilFinished: Long) {
Log.d("MYLOG", "text updated programmatically")
input = millisUntilFinished
}
override fun onFinish() {
input = 0
}
}
timer.start()
}
}
Here is my class with the compose into windowmanager
class WicController constructor(val context: Context?, val viewModel: WicViewModel) {
private val TAG = "WicController"
private var windowManager: WindowManager? = null
private var wicView: View? = null
private lateinit var lifecycleOwner: CDOLifecycleOwner
fun show() {
if (context == null) return
val paramFloat = WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
getWindowType(),
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT
)
paramFloat.gravity = Gravity.CENTER or Gravity.END
windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager?
val composeView = ComposeView(context.applicationContext)
composeView.setContent {
timerText(viewModel)
}
lifecycleOwner = CDOLifecycleOwner()
lifecycleOwner.performRestore(null)
lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
ViewTreeLifecycleOwner.set(composeView, lifecycleOwner)
ViewTreeSavedStateRegistryOwner.set(composeView, lifecycleOwner)
windowManager?.addView(composeView, paramFloat)
}
fun hide() {
windowManager?.removeView(wicView!!)
context?.let { Demo.startCalldorado(it) }
}
private fun getWindowType(): Int {
var windowType = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
windowType = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
}
return windowType
}
#Composable
fun timerText(viewModel: WicViewModel){
Text(
text = (viewModel.input / 1000).toString(),
modifier = Modifier
.padding(bottom = 8.dp)
.fillMaxWidth(),
fontSize = 13.sp,
color = Color.White,
)
}}
I know that the value is being changed but the compose is not recomposing itself
sometimes it may change but it very random.
I dont know if im doing it wrong or if compose is not made for windowmanager/floating window use, however I hope some of you guys on here can help me out since I cant find anything on the web about this.
I'd like to make my app displays texts one by one, with typewriter effect. I made a custom view for the effect. When the first custom view finishes displaying the texts, my app would wait for a click on the screen, or the layout. On the click, the next custom view starts to display texts, and when it finishes it waits for a click, and so on.
This is the custom view I made:
class TypeWriterView: AppCompatTextView {
private var mText: CharSequence? = null
private var mIndex = 0
private var mDelay: Long = 150 // in ms
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
private val mHandler: Handler = Handler()
private val characterAdder: Runnable = object : Runnable {
override fun run() {
text = mText!!.subSequence(0, mIndex++)
if (mIndex <= mText!!.length) {
mHandler.postDelayed(this, mDelay)
}
}
}
fun commence(txt: CharSequence?, delay: Long) {
mText = txt
mIndex = 0
text = ""
mDelay = delay
mHandler.removeCallbacks(characterAdder)
mHandler.postDelayed(characterAdder, mDelay)
}
}
To make it work like I mentioned above, I thought it would need to pass some values to activity(in my case it's actually a fragment) on finish, so I searched some and put some code in run() method.
override fun run() {
val job = GlobalScope.launch(Dispatchers.Default) {
text = mText!!.subSequence(0, mIndex++)
if (mIndex <= mText!!.length) {
mHandler.postDelayed(**this**, mDelay)
}
}
runBlocking {
job.join()
// pass value after finishing to display texts
}
}
Then "this" gets red underline with the error message saying:
Type mismatch.
Required: Runnable
Found: CoroutineScope
So what should I put here instead of "this"? Or are there any better way to do this?
The Handler is of no use when you are using coroutines.
The delay() should be used to delay the execution, and you don't need to push Runnable to the thread again and again.
class TypeWriterView: AppCompatTextView {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
suspend fun commence(txt: CharSequence?, delayMs: Long) {
if (txt == null) return
var index = 0
while (index < txt.length) {
text = txt.subSequence(0, ++index)
delay(delayMs)
}
}
// Get [Job] reference after launching in [owner]
fun commenceOnLifecycle(owner: LifecycleOwner, txt: CharSequence?, delayMs: Long): Job =
owner.lifecycleScope.launch { commence(txt, delayMs) }
}
LifecycleOwner is implemented by Activity, Fragment, etc. whichever has a lifecycle attached (more info: LifecycleOwner), it dispatches job to Dispatchers.Main by default so UI can be changed.
I have the following class that I am trying to test. The method I am having problems with is the showScollerView as I am trying to stub/mock the behaviour and then verify the behaviour in the test.
class CustomScrollerView #JvmOverloads constructor(
context: Context,
attributeSet: AttributeSet? = null,
styleAttributes: Int = 0)
: ConstraintLayout(context, attributeSet, styleAttributes) {
private var fragment: ConstraintLayout by Delegates.notNull()
private var layoutResEnding: Int = 0
private val transition = ChangeBounds()
private val constraintSet = ConstraintSet()
private var isShowing = false
init {
View.inflate(context, R.layout.overview_scroller_view, this)
transition.interpolator = AccelerateInterpolator()
transition.duration = 300
}
fun <L: ConstraintLayout> setView(view: L) {
fragment = view
}
fun setLayoutResourceFinish(#LayoutRes id: Int) {
layoutResEnding = id
}
fun showScrollerView() {
constraintSet.clone(context, layoutResEnding)
TransitionManager.beginDelayedTransition(fragment, transition)
constraintSet.applyTo(fragment)
isShowing = true
}
fun isScrollViewShowing() = isShowing
}
This is the test class
class CustomScrollerViewTest: RobolectricTest() {
#Mock
lateinit var constraintSet: ConstraintSet
#Mock
lateinit var constraintLayout: ConstraintLayout
private var customScrollerView: CustomScrollerView by Delegates.notNull()
#Before
fun setup() {
customScrollerView = CustomScrollerView(RuntimeEnvironment.application.baseContext)
}
#Test
fun `test that CustomScrollerView is not null`() {
assertThat(customScrollerView).isNotNull()
}
#Test
fun `test that the scrollerView is shown`() {
doNothing().`when`(constraintSet.clone(RuntimeEnvironment.application.baseContext, R.layout.fragment)) /* Error here */
doNothing().`when`(constraintSet).applyTo(constraintLayout)
customScrollerView.setLayoutResourceFinish(R.layout.fragment)
customScrollerView.setView(constraintLayout)
customScrollerView.showScrollerView()
assertThat(customScrollerView.isScrollViewShowing()).isEqualTo(true)
verify(constraintSet).applyTo(constraintLayout)
verify(constraintSet).clone(RuntimeEnvironment.application.baseContext, R.layout.fragment)
}
}
I get the error on this line:
doNothing().when(constraintSet.clone(RuntimeEnvironment.application.baseContext, R.layout.fragment))
This is the actual error message:
Unfinished stubbing detected here:
-> at com.nhaarman.mockito_kotlin.MockitoKt.doNothing(Mockito.kt:108)
E.g. thenReturn() may be missing.
Examples of correct stubbing:
when(mock.isOk()).thenReturn(true);
when(mock.isOk()).thenThrow(exception);
doThrow(exception).when(mock).someVoidMethod();
Hints:
1. missing thenReturn()
2. you are trying to stub a final method, which is not supported
3: you are stubbing the behaviour of another mock inside before 'thenReturn' instruction if completed
The line you are getting an error should be:
doNothing().`when`(constraintSet).clone(RuntimeEnvironment.application.baseContext, R.layout.fragment)
just like the example from the javadoc here:
List list = new LinkedList();
List spy = spy(list);
//let's make clear() do nothing
doNothing().when(spy).clear();
spy.add("one");
//clear() does nothing, so the list still contains "one"
spy.clear();
In hopes to better understand MVVM I decided to redo one of my Android projects implementing the architecture.
One of the activities uses the Zxing library to scan a QR code. Using Zxing's event handler I would like to pass the scanned result to my viewModel to check for example if the result is a "product"(this is a QR code I generate following a product class) or not. If the the result is a product it will show a dialogue with the result asking the user if they want to continue scanning or save the product, if it is not a product then it shows a dialogue with the result and a cancel button.
Now the issue here is that I don't know how to properly pass the data from Zxing's handler to the viewModel. Below attached my activity.
class QRreader_Activity : AppCompatActivity(), ZXingScannerView.ResultHandler, IQRreader{
override fun isProduct(ProductDetail: String) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun isNotProduct(ScannedText: String) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
lateinit var mScannerView: ZXingScannerView
var data: String="test"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_qrreader)
val bReturn: ImageButton = findViewById(R.id.ic_return)
bReturn.setOnClickListener {
onBackPressed() }
ActivityCompat.requestPermissions(this#QRreader_Activity,
arrayOf(Manifest.permission.CAMERA),
1)
val flashlightCheckBox = findViewById<CheckBox>(R.id.flashlight_checkbox)
val contentFrame = findViewById<ViewGroup>(R.id.content_frame)
mScannerView = object : ZXingScannerView(this) {
override fun createViewFinderView(context: Context): IViewFinder {
return CustomViewFinderView(context)
}
}
contentFrame.addView(mScannerView)
flashlightCheckBox.setOnCheckedChangeListener { compoundButton, isChecked -> mScannerView.flash = isChecked }
}
public override fun onResume() {
super.onResume()
mScannerView.setResultHandler(this) // Register ourselves as a handler for scan results.
mScannerView.startCamera() // Start camera on resume
}
public override fun onPause() {
super.onPause()
mScannerView.stopCamera() // Stop camera on pause
}
override fun onBackPressed() {
super.onBackPressed()
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
finish()
}
//This is where ZXing provides the result of the scan
override fun handleResult(rawResult: Result) {
Toast.makeText(this,""+rawResult.text,Toast.LENGTH_LONG).show()
}
private class CustomViewFinderView : ViewFinderView {
val PAINT = Paint()
constructor(context: Context) : super(context) {
init()
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
init()
}
private fun init() {
PAINT.color = Color.WHITE
PAINT.isAntiAlias = true
val textPixelSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
TRADE_MARK_TEXT_SIZE_SP.toFloat(), resources.displayMetrics)
PAINT.textSize = textPixelSize
setSquareViewFinder(true)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
drawTradeMark(canvas)
}
private fun drawTradeMark(canvas: Canvas) {
val framingRect = framingRect
val tradeMarkTop: Float
val tradeMarkLeft: Float
if (framingRect != null) {
tradeMarkTop = framingRect.bottom.toFloat() + PAINT.textSize + 10f
tradeMarkLeft = framingRect.left.toFloat()
} else {
tradeMarkTop = 10f
tradeMarkLeft = canvas.height.toFloat() - PAINT.textSize - 10f
}
canvas.drawText(TRADE_MARK_TEXT, tradeMarkLeft, tradeMarkTop, PAINT)
}
companion object {
val TRADE_MARK_TEXT = ""
val TRADE_MARK_TEXT_SIZE_SP = 40
}
}
}
The solution I found was to have the view model implement "ZXingScannerView.ResultHandler" interface. I don't know if this is the optimal solution though.