Microsoft MVP성태의 닷넷 이야기
Phone: 18. C# MAUI - 안드로이드 플랫폼에서의 Activity 제어 [링크 복사], [링크+제목 복사],
조회: 9604
글쓴 사람
정성태 (seongtaejeong at gmail.com)
홈페이지
첨부 파일
 
(연관된 글이 2개 있습니다.)
(시리즈 글이 4개 있습니다.)
Phone: 16. C# MAUI - /Download 등의 공용 디렉터리에 접근하는 방법
; https://www.sysnet.pe.kr/2/0/13631

Phone: 17. C# MAUI - Android 내에 Web 서비스 호스팅
; https://www.sysnet.pe.kr/2/0/13632

Phone: 18. C# MAUI - 안드로이드 플랫폼에서의 Activity 제어
; https://www.sysnet.pe.kr/2/0/13634

Phone: 21. C# MAUI - Android 환경에서의 파일 다운로드(DownloadManager)
; https://www.sysnet.pe.kr/2/0/13640




C# MAUI - 안드로이드 플랫폼에서의 Activity 제어

(이 글은 안드로이드 환경만을 대상으로 합니다.)

MAUI가 기본적으로 XAML을 사용한 UI를 지원하지만 전통적인 Activity를 사용하는 것도 가능합니다. 예를 하나 들어볼까요? 간단하게 Button Click을 하나 만들고,

<Button
        x:Name="btnNewActivity"
        Text="New Activity" 
        SemanticProperties.Hint="Click to show new Activity"
        Clicked="OnNewActivityClicked"
        HorizontalOptions="Fill" />

이벤트 핸들러에서 이런 식으로 Activity를 띄울 수 있습니다.

public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();
    }

    private void OnNewActivityClicked(object sender, EventArgs e)
    {
#if ANDROID
        var currentActivity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
        if (currentActivity == null)
        {
            return;
        }

        var newIntent = new Intent(currentActivity, typeof(NewActivity));
        currentActivity.StartActivity(newIntent);
#endif
    }
}

NewActivity 클래스는 .\Platforms\Android 경로에 대충 이렇게 추가해 두면 됩니다.

// NewActivity.cs

using Android.App;
using Android.OS;

namespace SimplePlayer;

[Activity]
internal class NewActivity : Activity
{
    protected override void OnCreate(Bundle? savedInstanceState)
    {
        base.OnCreate(savedInstanceState);
    }
}

당연히 이렇게만 하면 버튼을 클릭했을 때 뜨는 NewActivity 화면은 비어 있게 됩니다.




그나저나, 제가 Xamarin Forms부터 이어져온 .NET MAUI에 대한 이력을 전혀 모르고 있습니다. ^^; 게다가 구조도 아직 눈에 안 들어와서 초보 사용자 수준으로만 다뤄보고 있는데요, 그래서 아직 모르는 것인지, 그런 방법이 없는 것인지 알 수 없지만 현재 .NET MAUI는 XAML을 이용한 폼 구성을 MainActivity에만 종속시키고 있는 듯합니다.

다시 말해, 제가 위에서 만든 NewActivity를 위해 XAML로 UI를 구성할 수 없다는 것입니다. (혹시 방법을 아시는 분은 덧글 부탁드립니다. ^^)

이와 관련해서는 검색해도 자료도 없고, 어느 정도 저렇다고 판단할 수밖에 없는 근거가 있다면, 바로 App.Current에 대한 접근이 static 필드라는 사실입니다.

public static Application? Current { get; set; }

여기서 Current는 .NET MAUI 프로젝트의 App.xaml/App.xaml.cs에 해당하는 인스턴스가 설정되는데요, 그렇다면 오직 하나의 XAML Application 구조만 생성된다는 것이고 그것이 MainActivity가 소유한 Window에만 종속돼 있을 것입니다. 이런 상황에서 다른 Activity에서 (이미 MainActivity가 소유한) XAML을 임의로 가져가서 쓴다는 것이 맞지 않습니다. 애초에 이게 가능하려면 AppShell이나 App이 다중으로 생성될 수 있는 구조여야 하고 그것과 Activity가 연결되었다는 정보가 있어야만 합니다.

하지만, .NET MAUI가 생성하는 MainActivity는 딱히 AppShell/MainPage XAML을 소유한다는 식의 정보는 소스코드 내에 어떤 식으로도 명시돼 있지 않습니다.

[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true,
    ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density
    )]
public class MainActivity : MauiAppCompatActivity
{
    protected override void OnCreate(Bundle? savedInstanceState)
    {
        base.OnCreate(savedInstanceState);
    }
}




자, 그래서 우리가 새로 생성한 Activity인 경우에는 전통적인 Android 방식의 UI 시스템을 따라야 합니다. 이를 위해 .\Platforms\Android\Resources\layout 디렉터리에 activity_new.axml 파일을 만들어 다음의 내용으로 채웁니다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical" >
    <TextView android:id="@+id/txtOutput"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:text="Hello, world" />
    <Button android:id="@+id/btnSample"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Click" />
</LinearLayout>

그다음 NewActivity에서는 위의 layout을 이렇게 연결하면 됩니다.

[Activity]
internal class NewActivity : Activity
{
    TextView? _txtOutput;
    Android.Widget.Button? _btnSample;

    protected override void OnCreate(Bundle? savedInstanceState)
    {
        base.OnCreate(savedInstanceState);

        this.SetContentView(Resource.Layout.activity_new);

        _txtOutput = FindViewById<TextView>(Resource.Id.txtOutput);
        _btnSample = FindViewById<Android.Widget.Button>(Resource.Id.btnSample);

        if (_btnSample != null)
        {
            _btnSample.Click += _btnSample_Click;
        }
    }

    private void _btnSample_Click(object? sender, EventArgs e)
    {
        _txtOutput.Text = "Hello, .NET MAUI";
    }
}




그런데 여기서 한 가지 재미있는 점이 있습니다. (XAML과 연결된) MainActivity를 제외하고는, 위와 같이 임의로 생성한 Activity는 MAUI의 Navigation 기능인 Shell.Current.GoToAsync를 사용할 수 없습니다.

일례로, _btnSample_Click에서 다음과 같이 코드를 추가하고,

private async void _btnSample_Click(object? sender, EventArgs e)
{
    _txtOutput.Text = "Hello, .NET MAUI";

    await Shell.Current.GoToAsync($"//{nameof(MainPage)}");
}

실행 후, 버튼을 누르면 MainPage로의 전환이 되지 않습니다. 아래의 질문도 이와 관련된 것인데요,

Navigate to a page in .Net MAUI Anroid Activity
; https://stackoverflow.com/questions/77265785/navigate-to-a-page-in-net-maui-anroid-activity

다시 말해 저렇게 동작할 수는 없습니다. 사실 일면 이해는 됩니다. 위의 구조를 Windows 운영체제로 대입해 보면, WPF Window 내에 Page XAML이 구성돼 있는데 그 인스턴스를 그대로 다른 WPF Window의 Child로 동시에 종속시키는 것이 가능하지 않는 식입니다.

즉, 위의 명령어 실행은 Android의 axml layout UI 화면을 (이미 다른 Activity가 소유한) XAML 화면을 가져와 현재 Activity의 UI로 바꾸려는 시도가 됩니다.

그럼 어떻게 MainPage XAML로 갈 수 있을까요? 간단합니다. Activity 전환을 하면 됩니다. 따라서, Android의 방식에 따라 다음과 같이 Activity를 이동시켜야 합니다.

private void _btnSample_Click(object? sender, EventArgs e)
{
    // await Shell.Current.GoToAsync($"//{nameof(MainPage)}");

    var mainIntent = new Intent(this, typeof(MainActivity));
    StartActivity(mainIntent);
}




그런데, 여기서 또 한 가지 문제가 발생하는데요, 저 코드를 .NET 8/MAUI에서 실행하면 이런 오류 메시지를 보게 될 것입니다.

System.InvalidOperationException: 'Window was already created'

0xFFFFFFFFFFFFFFFF in Android.Runtime.RuntimeNativeMethods.monodroid_debugger_unhandled_exception   C#
0x1A in Android.Runtime.JNINativeWrapper._unhandled_exception at /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Android.Runtime/JNINativeWrapper.g.cs:13,5 C#
0x1E in Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PPL_V at /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Android.Runtime/JNINativeWrapper.g.cs:126,26  C#
0x12 in Microsoft.Maui.Controls.Window.Microsoft.Maui.IWindow.Created at D:\a\_work\1\s\src\Controls\src\Core\Window\Window.cs:483,5    C#
0xB in Microsoft.Maui.LifecycleEvents.AppHostBuilderExtensions.<>c.<OnConfigureLifeCycle>b__2_0 at D:\a\_work\1\s\src\Core\src\Hosting\LifecycleEvents\AppHostBuilderExtensions.Android.cs:23,6 C#
0xD in Microsoft.Maui.MauiAppCompatActivity. at D:\a\_work\1\s\src\Core\src\Platform\Android\MauiAppCompatActivity.Lifecycle.cs:61,104  C#
0x1F in Microsoft.Maui.LifecycleEvents.LifecycleEventServiceExtensions.InvokeLifecycleEvents<Microsoft.Maui.LifecycleEvents.AndroidLifecycle.OnPostCreate> at D:\a\_work\1\s\src\Core\src\LifecycleEvents\LifecycleEventServiceExtensions.cs:31,5 C#
0x40 in Microsoft.Maui.MauiAppCompatActivity.OnPostCreate at D:\a\_work\1\s\src\Core\src\Platform\Android\MauiAppCompatActivity.Lifecycle.cs:61,4   C#
0x11 in Android.App.Activity.n_OnPostCreate_Landroid_os_Bundle_ at /Users/runner/work/1/s/xamarin-android/src/Mono.Android/obj/Release/net8.0/android-34/mcw/Android.App.Activity.cs:4488,4 C#
0x9 in Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PPL_V at /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Android.Runtime/JNINativeWrapper.g.cs:125,5    C#

이슈를 보면 .NET 7 이하로는 괜찮았던 것 같은데 .NET 8부터 문제가 된다고 합니다.

MAUI Android build crashes when app is reopened from background. It throws the exception: 'Window was already created.' #18692
; https://github.com/dotnet/maui/issues/18692

위의 문제를 우회하는 방법이 하나 있는데요, 결국 OnPostCreate에서 예외가 발생한 것이기 때문에 아래와 같이 try/catch를 감싸주면 넘어가긴 합니다.

[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true,
    ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density
    )]
public class MainActivity : MauiAppCompatActivity
{
    protected override void OnCreate(Bundle? savedInstanceState)
    {
        base.OnCreate(savedInstanceState);
    }

    protected override void OnPostCreate(Bundle? savedInstanceState)
    {
        try
        {
            base.OnPostCreate(savedInstanceState);
        }
        catch (Exception e) // 하지만, 내부 코드의 어느 지점에서 예외가 발생했는지에 대해서는 알 수 없으므로 어떤 초기화가 생략됐을지 분석이 필요합니다.
        {
            System.Diagnostics.Trace.WriteLine(e.ToString());
        }
    }
}

위와 같은 현상이 발생하는 원인을 추측해 볼까요? 일단 예제 코드처럼 Intent 전환을 하면 Android는 새롭게 MainActivity 인스턴스를 생성해 처리합니다. 다시 말해 아래와 같은 Navigation Stack이 쌓여 있는 것입니다.

2: MainActivity // StartActivity(mainIntent)로 새로 쌓인 Activity
1: NewActivity  // currentActivity.StartActivity(newIntent)로 쌓인 Activity
0: MainActivity // 최초 App 실행 시 생성된 Activity

그렇다면 2개의 MainActivity 인스턴스가 생성됐고 XAML UI도 각각 새롭게 MainActivity에 붙어야 할 텐데요, 오류 메시지(Window was already created.)에서 알 수 있듯이 이미 한번 생성한 XAML UI를 중복 생성하는 바람에 예외 처리가 된 것입니다.

그래도 저 코드가 .NET 7 이하에서는 잘 동작했다고 하는 것을 보면 그전에는 (동작은 최초 생성한 MainActivity의 상태는 아니었겠지만) 중복 생성을 허용했었던 것을 .NET 8부터는 막은 것이 아닌가 생각됩니다. 물론 저만의 추측인데요, 그렇긴 한데 마이크로소프트는 저 이슈를 별다른 패치 없이 (아래에서 설명할 LaunchMode를 이용하라면서) 그냥 닫아버렸습니다.

그런 면에서 본다면, 엄밀히 MainActivity는 다중 인스턴스로 생성될 의미가 없습니다. 즉, 언제나/항상/무조건 LaunchMode 옵션을 SingleTask로 설정하는 것이 맞습니다.

[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true,
    ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density
    , LaunchMode = LaunchMode.SingleTask // 참고: "그림으로 이해하는 Activity LaunchMode 실험"
    )]
public class MainActivity : MauiAppCompatActivity
{
    // ...[생략]...
}

저렇게 바꾸고 나면, 이제야 (OnPostCreate에서의 try/catch를 제거해도) 이 글의 예제에서 작성한 코드들이 아무런 문제 없이 동작하게 됩니다.




참고로, (SingleTask를 사용하지 않고) OnPostCreate에서의 try/catch를 사용하는 우회 방법이 통하지 않는 경우가 있습니다. 바로 MediaElement를 추가했을 때인데요,

<!-- 참고: C# - MAUI에서 MediaElement 사용 -->

<toolkit:MediaElement x:Name="mediaPlayer" />

재미있는 건 저렇게만 사용했을 때는 문제없지만 Event Handler를 걸게 되면,

<toolkit:MediaElement x:Name="mediaPlayer" Loaded="OnMediaLoaded" />

이제부터는 다른 Activity에서 MainActivity로 돌아올 때 이런 예외가 발생합니다.

System.ObjectDisposedException: 'Cannot access a disposed object.
Object name: 'CommunityToolkit.Maui.Core.Views.MauiMediaElement'.'

0xFFFFFFFFFFFFFFFF in Android.Runtime.RuntimeNativeMethods.monodroid_debugger_unhandled_exception   C#
0x1A in Android.Runtime.JNINativeWrapper._unhandled_exception at /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Android.Runtime/JNINativeWrapper.g.cs:13,5 C#
0x23 in Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PPLLL_L at /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Android.Runtime/JNINativeWrapper.g.cs:368,26    C#
0x2E in Java.Interop.JniPeerMembers.AssertSelf at /Users/runner/work/1/s/xamarin-android/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.cs:153,5    C#
0x1 in Java.Interop.JniPeerMembers.JniInstanceMethods.InvokeVirtualVoidMethod at /Users/runner/work/1/s/xamarin-android/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods_Invoke.cs:57,5    C#
...[생략]...
0x5A in Microsoft.Maui.Platform.MauiContextExtensions.ToPlatform at D:\a\_work\1\s\src\Core\src\Platform\Android\MauiContextExtensions.cs:96,4  C#
0x2A in Microsoft.Maui.Controls.Platform.Compatibility.ShellFragmentContainer.OnCreateView at D:\a\_work\1\s\src\Controls\src\Core\Compatibility\Handlers\Shell\Android\ShellFragmentContainer.cs:26,4  C#
0x24 in AndroidX.Fragment.App.Fragment.n_OnCreateView_Landroid_view_LayoutInflater_Landroid_view_ViewGroup_Landroid_os_Bundle_ at C:\a\_work\1\s\generated\androidx.fragment.fragment\obj\Release\net6.0-android\generated\src\AndroidX.Fragment.App.Fragment.cs:2031,4 C#
0xD in Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PPLLL_L at /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Android.Runtime/JNINativeWrapper.g.cs:367,5  C#

이번에는 OnPostCreate 단계에서 발생하지 않기 때문에 응용 프로그램은 예외로 인해 종료하게 됩니다. 아마도 이런 문제 때문인지는 알 수 없으나, MediaElement는 애당초 문서에서 LaunchMode를 SingleTask로 지정하라고 명시하고 있습니다.

Platform specific initialization
; https://learn.microsoft.com/en-us/dotnet/communitytoolkit/maui/views/mediaelement?tabs=android#platform-specific-initialization

To initialize the MediaElement on Android, the LaunchMode of the applications Activity must be set to LaunchMode.SingleTask and you must add ResizableActivity=true as per the following example


언젠가는 마이크로소프트가 다중 XAML 초기화를 Activity마다 지원하게 될 수도 있을 것입니다. 적어도 그때까지는, 이런저런 이유로 인해 MainActivity의 LaunchMode는 항상 SingleTask로 지정하는 것이 여러모로 정신 건강에 이로울 듯합니다.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 6/6/2024]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2024-11-21 09시19분
Android쪽에서 activity 접근 할때 아래꺼 적어 놓고 참고 하고 있습니다. (Maui)
            //var context = Platform.AppContext;
            //Current Activity or null if not initialized or not started.
            //var activity = Platform.CurrentActivity;
            //var activity = await Platform.WaitForActivityAsync();
지현명

... 151  152  153  154  155  156  157  158  159  160  161  162  [163]  164  165  ...
NoWriterDateCnt.TitleFile(s)
972정성태1/7/201124194개발 환경 구성: 95. SQL Server 2008 R2 이하 버전 정보 확인
971정성태1/5/201133764.NET Framework: 199. .NET 코드 - Named Pipe 닷넷 서버와 VC++ 클라이언트 제작 [2]파일 다운로드1
970정성태1/4/201134297.NET Framework: 198. 윈도우 응용 프로그램에 Facebook 로그인 연동 [1]파일 다운로드1
969정성태12/31/201040365VC++: 45. Winsock 2 Layered Service Provider - Visual Studio 2010용 프로젝트 [1]파일 다운로드1
968정성태12/30/201026653개발 환경 구성: 94. 개발자가 선택할 수 있는 윈도우에서의 네트워크 프로그래밍 기술 [2]
967정성태12/27/201028426.NET Framework: 197. .NET 코드 - 단일 Process 실행파일 다운로드1
966정성태12/26/201026380.NET Framework: 196. .NET 코드 - 창 흔드는 효과파일 다운로드1
965정성태12/25/201027892개발 환경 구성: 93. MSBuild를 이용한 닷넷 응용프로그램의 다중 어셈블리 출력 빌드파일 다운로드1
964정성태12/21/2010143049개발 환경 구성: 92. 윈도우 서버 환경에서, 최대 생성 가능한 소켓(socket) 연결 수는 얼마일까? [14]
963정성태12/13/201027916개발 환경 구성: 91. MSBuild를 이용한 닷넷 응용프로그램의 플랫폼(x86/x64)별 빌드 [2]파일 다운로드1
962정성태12/10/201022766오류 유형: 110. GAC 등록 - Failure adding assembly to the cache: Invalid file or assembly name.
961정성태12/10/201099811개발 환경 구성: 90. 닷넷에서 접근해보는 PostgreSQL DB [5]
960정성태12/8/201045135.NET Framework: 195. .NET에서 코어(Core) 관련 CPU 정보 알아내는 방법파일 다운로드1
959정성태12/8/201031957.NET Framework: 194. Facebook 연동 - API Error Description: Invalid OAuth 2.0 Access Token
958정성태12/7/201028960개발 환경 구성: 89. 배치(batch) 파일에서 또 다른 배치 파일을 동기 방식으로 실행 및 반환값 얻기 [2]
957정성태12/6/201031718디버깅 기술: 31. Windbg - Visual Studio 디버그 상태에서 종료해 버리는 응용 프로그램 [3]
953정성태11/28/201036930.NET Framework: 193. 페이스북(Facebook) 계정으로 로그인하는 C# 웹 사이트 제작 [5]
952정성태11/25/201025357.NET Framework: 192. GC의 부하는 상대적인 것! [4]
950정성태11/18/201076726.NET Framework: 191. ClickOnce - 관리자 권한 상승하는 방법 [17]파일 다운로드2
954정성태11/29/201048716    답변글 .NET Framework: 191.1. [답변] 클릭원스 - 요청한 작업을 수행하려면 권한 상승이 필요합니다. (Exception from HRESULT: 0x800702E4) [2]
949정성태11/16/201027254오류 유형: 109. System.ServiceModel.Security.SecurityNegotiationException
948정성태11/16/201036086.NET Framework: 190. 트위터 계정으로 로그인하는 C# 웹 사이트 제작 [7]파일 다운로드1
947정성태11/14/201041698.NET Framework: 189. Mono Cecil로 만들어 보는 .NET Decompiler [1]파일 다운로드1
946정성태11/11/201041554.NET Framework: 188. .NET 64비트 응용 프로그램에서 왜 (2GB) OutOfMemoryException 예외가 발생할까? [1]파일 다운로드1
945정성태11/11/201025061VC++: 44. C++/CLI 컴파일 오류 - error C4368: mixed types are not supported
944정성태11/11/201031590VC++: 43. C++/CLI 컴파일 오류 - error C2872: 'IServiceProvider' : ambiguous symbol could be ...
... 151  152  153  154  155  156  157  158  159  160  161  162  [163]  164  165  ...