Microsoft MVP성태의 닷넷 이야기
Phone: 18. C# MAUI - 안드로이드 플랫폼에서의 Activity 제어 [링크 복사], [링크+제목 복사],
조회: 9631
글쓴 사람
정성태 (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();
지현명

... 121  122  123  124  125  126  127  128  129  130  [131]  132  133  134  135  ...
NoWriterDateCnt.TitleFile(s)
1780정성태10/15/201424224오류 유형: 249. The application-specific permission settings do not grant Local Activation permission for the COM Server application with CLSID
1779정성태10/15/201419754오류 유형: 248. Active Directory에서 OU가 지워지지 않는 경우
1778정성태10/10/201418205오류 유형: 247. The Netlogon service could not create server share C:\Windows\SYSVOL\sysvol\[도메인명]\SCRIPTS.
1777정성태10/10/201421321오류 유형: 246. The processing of Group Policy failed. Windows attempted to read the file \\[도메인]\sysvol\[도메인]\Policies\{...GUID...}\gpt.ini
1776정성태10/10/201418334오류 유형: 245. 이벤트 로그 - Name resolution for the name _ldap._tcp.dc._msdcs.[도메인명]. timed out after none of the configured DNS servers responded.
1775정성태10/9/201419457오류 유형: 244. Visual Studio 디버깅 (2) - Unable to break execution. This process is not currently executing the type of code that you selected to debug.
1774정성태10/9/201426648개발 환경 구성: 246. IIS 작업자 프로세스의 20분 자동 재생(Recycle)을 끄는 방법
1773정성태10/8/201429799.NET Framework: 471. 웹 브라우저로 다운로드가 되는 파일을 왜 C# 코드로 하면 안되는 걸까요? [1]
1772정성태10/3/201418613.NET Framework: 470. C# 3.0의 기본 인자(default parameter)가 .NET 1.1/2.0에서도 실행될까? [3]
1771정성태10/2/201428113개발 환경 구성: 245. 실행된 프로세스(EXE)의 명령행 인자를 확인하고 싶다면 - Sysmon [4]
1770정성태10/2/201421715개발 환경 구성: 244. 매크로 정의를 이용해 파일 하나로 C++과 C#에서 공유하는 방법 [1]파일 다운로드1
1769정성태10/1/201424136개발 환경 구성: 243. Scala 개발 환경 구성(JVM, 닷넷) [1]
1768정성태10/1/201419559개발 환경 구성: 242. 배치 파일에서 Thread.Sleep 효과를 주는 방법 [5]
1767정성태10/1/201424649VS.NET IDE: 94. Visual Studio 2012/2013에서의 매크로 구현 - Visual Commander [2]
1766정성태10/1/201422514개발 환경 구성: 241. 책 "프로그래밍 클로저: Lisp"을 읽고 나서. [1]
1765정성태9/30/201426062.NET Framework: 469. Unity3d에서 transform을 변수에 할당해 사용하는 특별한 이유가 있을까요?
1764정성태9/30/201422307오류 유형: 243. 파일 삭제가 안 되는 경우 - The action can't be comleted because the file is open in System
1763정성태9/30/201423877.NET Framework: 468. PDB 파일을 연동해 소스 코드 라인 정보를 알아내는 방법파일 다운로드1
1762정성태9/30/201424555.NET Framework: 467. 닷넷에서 EIP/RIP 레지스터 값을 구하는 방법 [1]파일 다운로드1
1761정성태9/29/201421609.NET Framework: 466. 윈도우 운영체제의 보안 그룹 이름 및 설명 문자열을 바꾸는 방법파일 다운로드1
1760정성태9/28/201419892.NET Framework: 465. ICorProfilerInfo::GetILToNativeMapping 메서드가 0x80131358을 반환하는 경우
1759정성태9/27/201430995개발 환경 구성: 240. Visual C++ / x64 환경에서 inline-assembly를 매크로 어셈블리로 대체하는 방법파일 다운로드1
1758정성태9/23/201437897개발 환경 구성: 239. 원격 데스크톱 접속(RDP)을 기존의 콘솔 모드처럼 사용하는 방법 [1]
1757정성태9/23/201418437오류 유형: 242. Lync로 모임 참여 시 소리만 들리지 않는 경우 - 두 번째 이야기
1756정성태9/23/201427468기타: 48. NVidia 제품의 과다한 디스크 사용 [2]
1755정성태9/22/201434250오류 유형: 241. Unity Web Player를 설치해도 여전히 설치하라는 화면이 나오는 경우 [4]
... 121  122  123  124  125  126  127  128  129  130  [131]  132  133  134  135  ...