Repository: Pyozer/introduction_screen

Issue:
Bug: Can't override button style shape
<!--- Provide a general summary of the issue in the Title above -->

It is currently impossible to change the shape of intro_buttons using baseBtnStyle or nextStyle (same applies for all style attributes). 

<!--- More than 50 issues open?  -->
<!--- Help us reduce the work first :-) -->
**Describe the bug**
<!--- If describing a bug, tell us what happens instead of the expected behavior -->

This is due to the fact that [ButtonStyle.merge from Flutter](https://api.flutter.dev/flutter/material/ButtonStyle/merge.html) takes precedence on the caller objects non-null values over the style argument and the merge order in the intro_button.dart implementation: https://github.com/Pyozer/introduction_screen/blob/master/lib/src/ui/intro_button.dart#L26C1-L30C26

```dart
          style: TextButton.styleFrom(
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(8.0),
            ),
          ).merge(style),
```


**To Reproduce**
Steps to reproduce the behavior:
1. Create an IntroductionScreen instance with the following BaseBtnStyle:
   ```dart
      baseBtnStyle: TextButton.styleFrom(
        foregroundColor: Colors.red,
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(8.0),
          side: BorderSide(),
        ),
      ),
   ```
2. See that the text color has changed to red but the button has no border applied.

**Expected behavior**
<!--- If you're describing a bug, tell us what should happen -->
<!--- If you're suggesting a change/improvement, tell us how it should work -->

I expect to be able to override the shape of buttons using ButtonStyle.

I see two options in solving this:

a) the most backwards-compatible solution would be to still merge the styles but change the order so that the custom style overrules the default style:

```diff
--- a/lib/src/ui/intro_button.dart
+++ b/lib/src/ui/intro_button.dart
@@ -16,6 +16,11 @@ class IntroButton extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
+    final ButtonStyle defaultStyle = TextButton.styleFrom(
+      shape: RoundedRectangleBorder(
+        borderRadius: BorderRadius.circular(8.0),
+      ),
+    );
     return MergeSemantics(
       child: Semantics(
         label: semanticLabel,
@@ -23,11 +28,7 @@ class IntroButton extends StatelessWidget {
         child: TextButton(
           onPressed: onPressed,
           child: child,
-          style: TextButton.styleFrom(
-            shape: RoundedRectangleBorder(
-              borderRadius: BorderRadius.circular(8.0),
-            ),
-          ).merge(style),
+          style: style?.merge(defaultStyle) ?? defaultStyle,
         ),
       ),
     );
```

b) the more straight-forward solution would be to only apply the default shape if no style is provided:


```diff
@@ -23,11 +23,12 @@ class IntroButton extends StatelessWidget {
         child: TextButton(
           onPressed: onPressed,
           child: child,
-          style: TextButton.styleFrom(
-            shape: RoundedRectangleBorder(
-              borderRadius: BorderRadius.circular(8.0),
-            ),
-          ).merge(style),
+          style: style ??
+              TextButton.styleFrom(
+                shape: RoundedRectangleBorder(
+                  borderRadius: BorderRadius.circular(8.0),
+                ),
+              ),
         ),
       ),
     );
```

**Screenshots**
<!--- Describe, in detail, what needs to happen to reproduce this bug -->
<!--- Give us a screenshot (if it's helpful for this particular bug) -->

Here you can see screenshots of the example how to reproduce and that the border is applied with either of the proposed changes:

Current:
![Image](https://github.com/user-attachments/assets/db087681-62bc-48f9-b80e-d81a86b184a7)

With proposed changes applied:
![Image](https://github.com/user-attachments/assets/7bc0a656-47ff-4820-ad7a-76abe21c9763)

**Smartphone (please complete the following information):**
- Device: should apply to all devices, shown on Pixel 6 or iPhone 15
- OS: n/a
- Version 3.1.14

**Additional context**
Add any other context about the problem here.




Fix patch:
diff --git a/.github/workflows/example_app.yml b/.github/workflows/example_app.yml
index ac2a8e2..820a5d3 100644
--- a/.github/workflows/example_app.yml
+++ b/.github/workflows/example_app.yml
@@ -15,7 +15,7 @@ jobs:
       - uses: actions/setup-java@v3
         with:
           distribution: 'temurin'
-          java-version: '11'
+          java-version: '17'
       - run: dart --version
       - run: flutter --version
       - run: flutter analyze
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 50d2119..7085a4b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [3.1.15] - 2025-02-12
+
+### Fixed
+- Fix button style can not be overridden [#224](https://github.com/Pyozer/introduction_screen/pull/224)
+
 ## [3.1.14] - 2024-03-23
 
 ### Fixed
diff --git a/README.md b/README.md
index 24c9cdf..3b1f5d2 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@ You just need to add `introduction_screen` as a [dependency in your pubspec.yaml
 
 ```yaml
 dependencies:
-  introduction_screen: ^3.1.14
+  introduction_screen: ^3.1.15
 ```
 
 ## Examples
diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties
index e1ca574..8bc9958 100644
--- a/example/android/gradle/wrapper/gradle-wrapper.properties
+++ b/example/android/gradle/wrapper/gradle-wrapper.properties
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip
diff --git a/example/android/settings.gradle b/example/android/settings.gradle
index 1d6d19b..0f9d8c9 100644
--- a/example/android/settings.gradle
+++ b/example/android/settings.gradle
@@ -19,7 +19,7 @@ pluginManagement {
 
 plugins {
     id "dev.flutter.flutter-plugin-loader" version "1.0.0"
-    id "com.android.application" version "7.3.0" apply false
+    id "com.android.application" version "8.1.0" apply false
     id "org.jetbrains.kotlin.android" version "1.7.10" apply false
 }
 
diff --git a/lib/src/ui/intro_button.dart b/lib/src/ui/intro_button.dart
index 94e6c96..759b82b 100644
--- a/lib/src/ui/intro_button.dart
+++ b/lib/src/ui/intro_button.dart
@@ -16,6 +16,12 @@ class IntroButton extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
+    final defaultStyle = TextButton.styleFrom(
+      shape: RoundedRectangleBorder(
+        borderRadius: BorderRadius.circular(8.0),
+      ),
+    );
+
     return MergeSemantics(
       child: Semantics(
         label: semanticLabel,
@@ -23,11 +29,7 @@ class IntroButton extends StatelessWidget {
         child: TextButton(
           onPressed: onPressed,
           child: child,
-          style: TextButton.styleFrom(
-            shape: RoundedRectangleBorder(
-              borderRadius: BorderRadius.circular(8.0),
-            ),
-          ).merge(style),
+          style: style?.merge(defaultStyle) ?? defaultStyle,
         ),
       ),
     );
diff --git a/pubspec.yaml b/pubspec.yaml
index 5857771..2967275 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,6 +1,6 @@
 name: introduction_screen
 description: Introduction/Onboarding package for flutter app with some customizations possibilities
-version: 3.1.14
+version: 3.1.15
 repository: https://github.com/pyozer/introduction_screen
 issue_tracker: https://github.com/pyozer/introduction_screen/issues
 


Test patch:
diff --git a/test/src/helper_test.dart b/test/src/helper_test.dart
index 64cef0b..67e2dc5 100644
--- a/test/src/helper_test.dart
+++ b/test/src/helper_test.dart
@@ -4,31 +4,26 @@ import 'package:introduction_screen/src/helper.dart';
 void main() {
   // Global variables
   final originalList = [1, 2, 3, 4, 5];
-  final list = ['a', 'b', 'c'];
 
   group('CustomList', () {
-    test('asReversed returns reversed list when isReverse is true', () {
-      final reversedList = originalList.asReversed(true);
-
-      expect(reversedList, [5, 4, 3, 2, 1]);
+    test('asReversed() reverses list when isReverse is true', () {
+      expect(originalList.asReversed(true), [5, 4, 3, 2, 1]);
     });
 
-    test('asReversed returns original list when isReverse is false', () {
-      final originalCopy = originalList.asReversed(false);
-
-      expect(originalCopy, originalList);
+    test('asReversed() returns original list when isReverse is false', () {
+      expect(originalList.asReversed(false), [1, 2, 3, 4, 5]);
     });
 
     test('elementAtOrNull returns the correct element for valid index', () {
-      expect(list.elementAtOrNull(1), 'b');
+      expect(originalList.elementAtOrNull(1), 2);
     });
 
     test('elementAtOrNull returns null for out of bounds index', () {
-      expect(list.elementAtOrNull(5), isNull);
+      expect(originalList.elementAtOrNull(5), isNull);
     });
 
     test('elementAtOrNull returns null for negative index', () {
-      expect(list.elementAtOrNull(-1), isNull);
+      expect(originalList.elementAtOrNull(-1), isNull);
     });
   });
 }
diff --git a/test/src/introduction_screen_test.dart b/test/src/introduction_screen_test.dart
index a746e7d..e614f9e 100644
--- a/test/src/introduction_screen_test.dart
+++ b/test/src/introduction_screen_test.dart
@@ -1,6 +1,6 @@
 import 'package:dots_indicator/dots_indicator.dart';
-import 'package:flutter_test/flutter_test.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
 import 'package:introduction_screen/introduction_screen.dart';
 
 void main() {
@@ -12,6 +12,8 @@ void main() {
     bool showSkipButton = false,
     bool showDoneButton = false,
     bool showNextButton = true,
+    bool showBackButton = false,
+    int initialPage = 0,
     int? autoScrollDuration,
   }) {
     return MaterialApp(
@@ -24,8 +26,11 @@ void main() {
         next: showNextButton ? Text("Next") : null,
         showSkipButton: showSkipButton,
         showDoneButton: showDoneButton,
+        showBackButton: showBackButton,
+        back: showBackButton ? Text("Back") : null,
         showNextButton: showNextButton,
         autoScrollDuration: autoScrollDuration,
+        initialPage: initialPage,
       ),
     );
   }
@@ -64,6 +69,29 @@ void main() {
       expect(find.text('Page 2'), findsOneWidget);
     });
 
+    testWidgets('Back button goes back to the previous page', (tester) async {
+      // Arrange
+      await tester.pumpWidget(createIntroductionScreen(
+        pages: [
+          PageViewModel(title: 'Page 1', body: 'Introduction 1'),
+          PageViewModel(title: 'Page 2', body: 'Introduction 2'),
+        ],
+        showBackButton: true,
+        initialPage: 1,
+      ));
+
+      expect(find.text('Page 1'), findsNothing);
+      expect(find.text('Page 2'), findsOneWidget);
+
+      // Act
+      await tester.tap(find.text('Back'));
+      await tester.pumpAndSettle();
+
+      // Assert
+      expect(find.text('Page 1'), findsOneWidget);
+      expect(find.text('Page 2'), findsNothing);
+    });
+
     testWidgets('Skip button triggers onSkip callback', (tester) async {
       // Arrange
       var skipTapped = false;
@@ -167,24 +195,40 @@ void main() {
     });
   });
 
-  testWidgets('Auto-scroll works as expected', (WidgetTester tester) async {
+  testWidgets('Auto-scroll advances one page at a time',
+      (WidgetTester tester) async {
     // Arrange
-    final pages = [
-      PageViewModel(title: 'Page 1', body: 'Introduction 1'),
-      PageViewModel(title: 'Page 2', body: 'Introduction 2'),
-    ];
-
-    await tester.pumpWidget(
-        createIntroductionScreen(pages: pages, autoScrollDuration: 5));
+    const autoScrollDuration = 2000;
+    await tester.pumpWidget(createIntroductionScreen(
+      pages: [
+        PageViewModel(title: 'Page 1', body: 'Introduction 1'),
+        PageViewModel(title: 'Page 2', body: 'Introduction 2'),
+        PageViewModel(title: 'Page 3', body: 'Introduction 3'),
+      ],
+      autoScrollDuration: autoScrollDuration,
+    ));
 
-    // Initial page should be Page 1
+    // Should still be at page 1 after 100 ms
+    await tester.pump(Duration(milliseconds: 100));
+    await tester.pumpAndSettle();
     expect(find.text('Page 1'), findsOneWidget);
+    expect(find.text('Page 2'), findsNothing);
+    expect(find.text('Page 3'), findsNothing);
 
-    // Simulate time passing to trigger auto-scroll
-    await tester.pump(const Duration(milliseconds: 10));
+    // Wait for first auto-scroll, should be on page 2 now
+    await tester.pump(Duration(milliseconds: autoScrollDuration + 100));
     await tester.pumpAndSettle();
 
-    // The auto-scroll should have moved to the next page
+    expect(find.text('Page 1'), findsNothing);
     expect(find.text('Page 2'), findsOneWidget);
+    expect(find.text('Page 3'), findsNothing);
+
+    // Wait for second auto-scroll, should be on page 3 now
+    await tester.pump(Duration(milliseconds: autoScrollDuration));
+    await tester.pumpAndSettle();
+
+    expect(find.text('Page 1'), findsNothing);
+    expect(find.text('Page 2'), findsNothing);
+    expect(find.text('Page 3'), findsOneWidget);
   });
 }
diff --git a/test/widget/intro_ui_test.dart b/test/widget/intro_ui_test.dart
index 35023a9..efef456 100644
--- a/test/widget/intro_ui_test.dart
+++ b/test/widget/intro_ui_test.dart
@@ -125,5 +125,37 @@ void main() {
       // Check that the image is rendered
       expect(find.byWidget(mockImage), findsOneWidget);
     });
+
+    testWidgets('IntroButton custom styles override default styles',
+        (tester) async {
+      // Create a custom style with a different border radius
+      final customStyle = TextButton.styleFrom(
+        shape: RoundedRectangleBorder(
+          borderRadius:
+              BorderRadius.circular(20.0), // Different from default 8.0
+        ),
+        backgroundColor: Colors.blue, // Additional property to verify
+      );
+
+      await tester.pumpWidget(testableWidget(
+        child: IntroButton(
+          child: const Text('Test Text'),
+          style: customStyle,
+        ),
+      ));
+
+      // Find the TextButton
+      final button = tester.widget<TextButton>(find.byType(TextButton));
+
+      // Get the shape from the button's style
+      final shape = button.style?.shape?.resolve({});
+      expect(shape, isA<RoundedRectangleBorder>());
+      final borderRadius = (shape as RoundedRectangleBorder).borderRadius;
+      expect(borderRadius, BorderRadius.circular(20.0));
+
+      // Verify the background color was also applied
+      final backgroundColor = button.style?.backgroundColor?.resolve({});
+      expect(backgroundColor, Colors.blue);
+    });
   });
 }